wave-agent-sdk 0.17.8 → 0.17.10

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 (76) hide show
  1. package/builtin/skills/settings/ENV.md +1 -1
  2. package/dist/agent.d.ts +5 -0
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +7 -0
  5. package/dist/managers/aiManager.d.ts +10 -4
  6. package/dist/managers/aiManager.d.ts.map +1 -1
  7. package/dist/managers/aiManager.js +214 -200
  8. package/dist/managers/backgroundTaskManager.js +1 -1
  9. package/dist/managers/bangManager.js +1 -1
  10. package/dist/managers/hookManager.js +2 -4
  11. package/dist/managers/mcpManager.d.ts.map +1 -1
  12. package/dist/managers/mcpManager.js +1 -3
  13. package/dist/managers/messageManager.d.ts.map +1 -1
  14. package/dist/managers/messageManager.js +1 -8
  15. package/dist/managers/toolManager.d.ts +13 -0
  16. package/dist/managers/toolManager.d.ts.map +1 -1
  17. package/dist/managers/toolManager.js +33 -12
  18. package/dist/services/aiService.d.ts.map +1 -1
  19. package/dist/services/aiService.js +20 -12
  20. package/dist/services/configurationService.d.ts.map +1 -1
  21. package/dist/services/configurationService.js +13 -0
  22. package/dist/services/jsonlHandler.d.ts +1 -1
  23. package/dist/services/jsonlHandler.d.ts.map +1 -1
  24. package/dist/services/jsonlHandler.js +7 -22
  25. package/dist/services/session.d.ts +2 -9
  26. package/dist/services/session.d.ts.map +1 -1
  27. package/dist/services/session.js +16 -46
  28. package/dist/tools/bashTool.d.ts.map +1 -1
  29. package/dist/tools/bashTool.js +3 -1
  30. package/dist/tools/editTool.d.ts.map +1 -1
  31. package/dist/tools/editTool.js +28 -1
  32. package/dist/tools/enterPlanMode.d.ts.map +1 -1
  33. package/dist/tools/enterPlanMode.js +14 -4
  34. package/dist/tools/exitPlanMode.d.ts.map +1 -1
  35. package/dist/tools/exitPlanMode.js +8 -0
  36. package/dist/tools/readTool.d.ts.map +1 -1
  37. package/dist/tools/readTool.js +18 -10
  38. package/dist/tools/types.d.ts +8 -2
  39. package/dist/tools/types.d.ts.map +1 -1
  40. package/dist/tools/writeTool.d.ts.map +1 -1
  41. package/dist/tools/writeTool.js +14 -1
  42. package/dist/types/agent.d.ts +0 -2
  43. package/dist/types/agent.d.ts.map +1 -1
  44. package/dist/types/config.d.ts +1 -0
  45. package/dist/types/config.d.ts.map +1 -1
  46. package/dist/utils/cacheControlUtils.d.ts +20 -15
  47. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  48. package/dist/utils/cacheControlUtils.js +69 -66
  49. package/dist/utils/constants.d.ts +1 -1
  50. package/dist/utils/constants.js +1 -1
  51. package/dist/utils/containerSetup.js +5 -6
  52. package/package.json +1 -1
  53. package/src/agent.ts +8 -0
  54. package/src/managers/aiManager.ts +292 -263
  55. package/src/managers/backgroundTaskManager.ts +1 -1
  56. package/src/managers/bangManager.ts +1 -1
  57. package/src/managers/hookManager.ts +6 -6
  58. package/src/managers/mcpManager.ts +2 -4
  59. package/src/managers/messageManager.ts +1 -9
  60. package/src/managers/toolManager.ts +37 -12
  61. package/src/services/aiService.ts +23 -12
  62. package/src/services/configurationService.ts +21 -0
  63. package/src/services/jsonlHandler.ts +7 -27
  64. package/src/services/session.ts +16 -57
  65. package/src/tools/bashTool.ts +3 -1
  66. package/src/tools/editTool.ts +31 -1
  67. package/src/tools/enterPlanMode.ts +15 -4
  68. package/src/tools/exitPlanMode.ts +10 -0
  69. package/src/tools/readTool.ts +20 -10
  70. package/src/tools/types.ts +15 -3
  71. package/src/tools/writeTool.ts +15 -1
  72. package/src/types/agent.ts +0 -2
  73. package/src/types/config.ts +1 -0
  74. package/src/utils/cacheControlUtils.ts +77 -95
  75. package/src/utils/constants.ts +1 -1
  76. package/src/utils/containerSetup.ts +7 -7
@@ -4,7 +4,6 @@ import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
4
4
  import { microcompactMessages } from "../utils/microcompact.js";
5
5
  import { parseTaskNotificationXml } from "../utils/notificationXml.js";
6
6
  import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
7
- import * as fs from "node:fs/promises";
8
7
  import { existsSync } from "node:fs";
9
8
  import type {
10
9
  GatewayConfig,
@@ -75,6 +74,11 @@ export class AIManager {
75
74
  private originalWorkdir: string;
76
75
  private consecutiveCompactionFailures: number = 0;
77
76
  private readonly maxTurns?: number;
77
+ /** Tracks file mtime/hash at read time for staleness detection on Edit/Write */
78
+ private readFileState = new Map<
79
+ string,
80
+ { mtime: number; hash: string; offset?: number; limit?: number }
81
+ >();
78
82
  /** Override tool_choice for this AI manager (e.g. for structured output) */
79
83
  public toolChoiceOverride?:
80
84
  | "auto"
@@ -238,32 +242,29 @@ export class AIManager {
238
242
  }
239
243
 
240
244
  /**
241
- * Build plan mode system-reminder messages to inject into the API message stream.
242
- * These are transient messages not stored in the message history.
243
- * This preserves prompt caching by keeping the system prompt constant.
245
+ * Add plan mode reminder as a persistent meta message to the message manager.
246
+ * Called before getMessages() so the stored message is included in the API call.
244
247
  */
245
- private buildPlanModeMessages(
248
+ private maybeAddPlanModeMessage(
246
249
  currentMode: PermissionMode | undefined,
247
- ): import("openai/resources.js").ChatCompletionMessageParam[] {
248
- const messages: import("openai/resources.js").ChatCompletionMessageParam[] =
249
- [];
250
- if (!this.permissionManager) return messages;
250
+ ): void {
251
+ if (!this.permissionManager) return;
251
252
 
252
253
  // Handle exit notification (one-time after leaving plan mode)
253
254
  if (this.permissionManager.getNeedsPlanModeExitAttachment()) {
254
255
  const planFilePath = this.permissionManager.getPlanFilePath();
255
256
  const planExists = planFilePath ? existsSync(planFilePath) : false;
256
- messages.push({
257
- role: "user",
257
+ this.messageManager.addUserMessage({
258
258
  content: buildExitedPlanModeReminder(planFilePath, planExists),
259
+ isMeta: true,
259
260
  });
260
261
  this.permissionManager.setNeedsPlanModeExitAttachment(false);
261
262
  }
262
263
 
263
- if (currentMode !== "plan") return messages;
264
+ if (currentMode !== "plan") return;
264
265
 
265
266
  const planFilePath = this.permissionManager.getPlanFilePath();
266
- if (!planFilePath) return messages;
267
+ if (!planFilePath) return;
267
268
 
268
269
  const planExists = existsSync(planFilePath);
269
270
 
@@ -272,25 +273,23 @@ export class AIManager {
272
273
  if (planMgr?.isPlanEntryReminderPending()) {
273
274
  if (this.permissionManager.hasExitedPlanModeInSession() && planExists) {
274
275
  // Re-entry: use small reminder
275
- messages.push({
276
- role: "user",
276
+ this.messageManager.addUserMessage({
277
277
  content: buildPlanModeReEntryReminder(planFilePath),
278
+ isMeta: true,
278
279
  });
279
280
  } else {
280
281
  // First entry: use full reminder
281
- messages.push({
282
- role: "user",
282
+ this.messageManager.addUserMessage({
283
283
  content: buildPlanModeReminder(
284
284
  planFilePath,
285
285
  planExists,
286
286
  !!this.subagentType,
287
287
  ),
288
+ isMeta: true,
288
289
  });
289
290
  }
290
291
  planMgr.consumePlanEntryReminder();
291
292
  }
292
-
293
- return messages;
294
293
  }
295
294
 
296
295
  public setIsLoading(isLoading: boolean): void {
@@ -470,6 +469,25 @@ export class AIManager {
470
469
  compactUsage,
471
470
  );
472
471
 
472
+ // Re-add plan mode reminder as persistent meta message after compaction
473
+ const postCompactMode = this.permissionManager?.getCurrentEffectiveMode(
474
+ this.getModelConfig().permissionMode,
475
+ );
476
+ if (postCompactMode === "plan") {
477
+ const planFilePath = this.permissionManager?.getPlanFilePath();
478
+ if (planFilePath) {
479
+ const planExists = existsSync(planFilePath);
480
+ this.messageManager.addUserMessage({
481
+ content: buildPlanModeReminder(
482
+ planFilePath,
483
+ planExists,
484
+ !!this.subagentType,
485
+ ),
486
+ isMeta: true,
487
+ });
488
+ }
489
+ }
490
+
473
491
  // 8. Track usage
474
492
  if (compactUsage && this.callbacks?.onUsageAdded) {
475
493
  this.callbacks.onUsageAdded(compactUsage);
@@ -569,26 +587,6 @@ export class AIManager {
569
587
  if (usedTokens >= POST_COMPACT_TOKEN_BUDGET) break;
570
588
  }
571
589
 
572
- // 2. Plan mode context
573
- const currentMode = this.permissionManager?.getCurrentEffectiveMode(
574
- this.getModelConfig().permissionMode,
575
- );
576
- if (currentMode === "plan") {
577
- const planFilePath = this.permissionManager?.getPlanFilePath();
578
- if (planFilePath) {
579
- let planExists = false;
580
- try {
581
- await fs.access(planFilePath);
582
- planExists = true;
583
- } catch {
584
- // Plan file doesn't exist yet
585
- }
586
- contextParts.push(
587
- `\n\n${buildPlanModeReminder(planFilePath, planExists, !!this.subagentType)}`,
588
- );
589
- }
590
- }
591
-
592
590
  // 4. Invoked skills context (with token budget, matching Claude Code)
593
591
  const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000;
594
592
  const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000;
@@ -784,6 +782,14 @@ export class AIManager {
784
782
  toolAbortController = this.toolAbortController!;
785
783
  }
786
784
 
785
+ // Get current permission mode
786
+ const currentMode = this.permissionManager?.getCurrentEffectiveMode(
787
+ this.getModelConfig().permissionMode,
788
+ );
789
+
790
+ // Add plan mode reminder as persistent meta message before getting messages
791
+ this.maybeAddPlanModeMessage(currentMode);
792
+
787
793
  // Get recent message history with microcompact applied
788
794
  const rawMessages = this.messageManager.getMessages();
789
795
  const microcompactedMessages = microcompactMessages(rawMessages, {
@@ -801,23 +807,12 @@ export class AIManager {
801
807
 
802
808
  logger?.debug("modelConfig in sendAIMessage", this.getModelConfig());
803
809
 
804
- // Get current permission mode and plan file path
805
- const currentMode = this.permissionManager?.getCurrentEffectiveMode(
806
- this.getModelConfig().permissionMode,
807
- );
808
810
  const toolsConfig = this.getFilteredToolsConfig();
809
811
  const toolNames = new Set(toolsConfig.map((t) => t.function.name));
810
812
  const filteredToolPlugins = this.toolManager
811
813
  .getTools()
812
814
  .filter((t) => toolNames.has(t.name));
813
815
 
814
- // Inject plan mode system-reminder messages (not system prompt)
815
- // This preserves prompt caching by keeping the system prompt constant
816
- const planModeMessages = this.buildPlanModeMessages(currentMode);
817
- if (planModeMessages.length > 0) {
818
- recentMessages.push(...planModeMessages);
819
- }
820
-
821
816
  let autoMemoryOptions: { directory: string; content: string } | undefined;
822
817
 
823
818
  if (this.getAutoMemoryEnabled()) {
@@ -1001,218 +996,52 @@ export class AIManager {
1001
996
  }
1002
997
 
1003
998
  if (toolCalls.length > 0) {
1004
- // Execute all tools in parallel using Promise.all
1005
- const toolExecutionPromises = toolCalls.map(
1006
- async (functionToolCall) => {
1007
- const toolId = functionToolCall.id || "";
1008
-
1009
- // Check if already interrupted, skip tool execution if so
1010
- if (
1011
- abortController.signal.aborted ||
1012
- toolAbortController.signal.aborted
1013
- ) {
1014
- return;
1015
- }
1016
-
1017
- const toolName = functionToolCall.function?.name || "";
1018
- // Safely parse tool parameters, handle tools without parameters
1019
- let toolArgs: Record<string, unknown> = {};
1020
- let jsonRecovered = false;
1021
- const argsString = functionToolCall.function?.arguments?.trim();
1022
-
1023
- if (!argsString || argsString === "") {
1024
- // Tool without parameters, use empty object
1025
- toolArgs = {};
1026
- } else {
1027
- let recoveredArgs = argsString;
1028
- try {
1029
- toolArgs = JSON.parse(argsString);
1030
- } catch {
1031
- // Attempt to recover truncated JSON (e.g., missing closing braces)
1032
- recoveredArgs = recoverTruncatedJson(argsString);
1033
- try {
1034
- toolArgs = JSON.parse(recoveredArgs);
1035
- jsonRecovered = true;
1036
- logger.warn(
1037
- `Recovered truncated JSON for tool "${toolName}"`,
1038
- );
1039
- } catch (parseError) {
1040
- let errorMessage = `Failed to parse tool arguments`;
1041
- if (result.finish_reason === "length") {
1042
- errorMessage +=
1043
- " (output truncated, please reduce your output)";
1044
- }
1045
- logger?.error(errorMessage, parseError);
1046
- this.messageManager.updateToolBlock({
1047
- id: toolId,
1048
- parameters: argsString,
1049
- result: errorMessage,
1050
- success: false,
1051
- error: errorMessage,
1052
- stage: "end",
1053
- name: toolName,
1054
- compactParams: "",
1055
- timestamp: Date.now(),
1056
- });
1057
- return;
1058
- }
1059
- }
1060
- }
999
+ // Partition tool calls into batches: consecutive concurrency-safe
1000
+ // tools run in parallel, non-safe tools (Edit, Write, MCP) run one
1001
+ // at a time to prevent read-modify-write races on the same file.
1002
+ const batches: {
1003
+ calls: ChatCompletionMessageFunctionToolCall[];
1004
+ safe: boolean;
1005
+ }[] = [];
1006
+ for (const call of toolCalls) {
1007
+ const toolName = call.function?.name || "";
1008
+ const safe = this.toolManager.isConcurrencySafe(toolName);
1009
+ const lastBatch = batches[batches.length - 1];
1010
+ if (lastBatch && lastBatch.safe && safe) {
1011
+ lastBatch.calls.push(call);
1012
+ } else {
1013
+ batches.push({ calls: [call], safe });
1014
+ }
1015
+ }
1061
1016
 
1062
- const compactParams = this.generateCompactParams(
1063
- toolName,
1064
- toolArgs,
1017
+ // Execute batches sequentially; within a safe batch, tools run in parallel
1018
+ for (const batch of batches) {
1019
+ if (
1020
+ abortController.signal.aborted ||
1021
+ toolAbortController.signal.aborted
1022
+ ) {
1023
+ break;
1024
+ }
1025
+ if (batch.calls.length === 1) {
1026
+ await this.executeToolCall(
1027
+ batch.calls[0],
1028
+ abortController,
1029
+ toolAbortController,
1030
+ result.finish_reason,
1065
1031
  );
1066
-
1067
- // Emit start stage for non-streaming tool calls
1068
- if (!this.stream) {
1069
- this.messageManager.updateToolBlock({
1070
- id: toolId,
1071
- stage: "start",
1072
- name: toolName,
1073
- compactParams,
1074
- parameters: argsString,
1075
- });
1076
- }
1077
-
1078
- // Emit running stage (tool execution about to start)
1079
- this.messageManager.updateToolBlock({
1080
- id: toolId,
1081
- stage: "running",
1082
- name: toolName,
1083
- compactParams,
1084
- parameters: argsString,
1085
- parametersChunk: "",
1086
- });
1087
-
1088
- try {
1089
- // Execute PreToolUse hooks before tool execution
1090
- const shouldExecuteTool = await this.executePreToolUseHooks(
1091
- toolName,
1092
- toolArgs,
1093
- toolId,
1094
- );
1095
-
1096
- // If PreToolUse hooks blocked execution, skip tool execution
1097
- if (!shouldExecuteTool) {
1098
- logger?.info(
1099
- `Tool ${toolName} execution blocked by PreToolUse hooks`,
1100
- );
1101
- return; // Skip this tool and return from this map function
1102
- }
1103
-
1104
- // Create tool execution context
1105
- const context: ToolContext = {
1106
- abortSignal: toolAbortController.signal,
1107
- backgroundTaskManager: this.backgroundTaskManager,
1108
- workdir: this.getWorkdir(),
1109
- originalWorkdir: this.originalWorkdir,
1110
- env: this.container.get<Record<string, string>>("MergedEnv"),
1111
- messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
1112
- sessionId: this.messageManager.getSessionId(),
1113
- toolCallId: toolId,
1114
- taskManager: this.taskManager,
1115
- onShortResultUpdate: (shortResult: string) => {
1116
- this.messageManager.updateToolBlock({
1117
- id: toolId,
1118
- shortResult,
1119
- stage: "running", // Keep it in running stage while updating shortResult
1120
- });
1121
- },
1122
- onResultUpdate: (result: string) => {
1123
- this.messageManager.updateToolBlock({
1124
- id: toolId,
1125
- result,
1126
- stage: "running", // Keep it in running stage while updating result
1127
- });
1128
- },
1129
- onCwdChange: async (newCwd: string) => {
1130
- const oldCwd = this.getWorkdir();
1131
- this.container.register("Workdir", newCwd);
1132
- this._onCwdChange?.(newCwd);
1133
- if (this.hookManager) {
1134
- const sessionId = this.messageManager.getSessionId();
1135
- const transcriptPath =
1136
- this.messageManager.getTranscriptPath();
1137
- const env = Object.fromEntries(
1138
- Object.entries(process.env).filter(
1139
- (e) => e[1] !== undefined,
1140
- ),
1141
- ) as Record<string, string>;
1142
- await this.hookManager.executeCwdChangedHooks(
1143
- oldCwd,
1144
- newCwd,
1145
- sessionId,
1146
- transcriptPath,
1147
- env,
1148
- );
1149
- }
1150
- },
1151
- };
1152
-
1153
- // Execute tool
1154
- const toolResult = await this.toolManager.execute(
1155
- functionToolCall.function?.name || "",
1156
- toolArgs,
1157
- context,
1158
- );
1159
-
1160
- // Build result content, adding truncation warning if JSON was recovered
1161
- let toolResultContent =
1162
- toolResult.content ||
1163
- (toolResult.error ? `Error: ${toolResult.error}` : "");
1164
- if (jsonRecovered) {
1165
- toolResultContent +=
1166
- "\n\nTool arguments were truncated (likely exceeded max output tokens). Please reduce your output or split into multiple tool calls.";
1167
- }
1168
-
1169
- // Update message state - tool execution completed
1170
- this.messageManager.updateToolBlock({
1171
- id: toolId,
1172
- parameters: argsString,
1173
- result: toolResultContent,
1174
- success: toolResult.success,
1175
- error: toolResult.error,
1176
- stage: "end",
1177
- name: toolName,
1178
- compactParams,
1179
- shortResult: toolResult.shortResult,
1180
- isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
1181
- startLineNumber: toolResult.startLineNumber,
1182
- timestamp: Date.now(),
1183
- });
1184
-
1185
- // Execute PostToolUse hooks after successful tool completion
1186
- await this.executePostToolUseHooks(
1187
- toolId,
1188
- toolName,
1189
- toolArgs,
1190
- toolResult,
1191
- );
1192
- } catch (toolError) {
1193
- const errorMessage =
1194
- toolError instanceof Error
1195
- ? toolError.message
1196
- : String(toolError);
1197
-
1198
- this.messageManager.updateToolBlock({
1199
- id: toolId,
1200
- parameters: JSON.stringify(toolArgs, null, 2),
1201
- result: `Tool execution failed: ${errorMessage}`,
1202
- success: false,
1203
- error: errorMessage,
1204
- stage: "end",
1205
- name: toolName,
1206
- compactParams,
1207
- isManuallyBackgrounded: false,
1208
- timestamp: Date.now(),
1209
- });
1210
- }
1211
- },
1212
- );
1213
-
1214
- // Wait for all tools to complete execution in parallel
1215
- await Promise.all(toolExecutionPromises);
1032
+ } else {
1033
+ await Promise.all(
1034
+ batch.calls.map((call) =>
1035
+ this.executeToolCall(
1036
+ call,
1037
+ abortController,
1038
+ toolAbortController,
1039
+ result.finish_reason,
1040
+ ),
1041
+ ),
1042
+ );
1043
+ }
1044
+ }
1216
1045
  }
1217
1046
 
1218
1047
  // Handle token statistics and message compaction
@@ -1584,6 +1413,206 @@ export class AIManager {
1584
1413
  }
1585
1414
  }
1586
1415
 
1416
+ /**
1417
+ * Execute a single tool call: arg parsing, block emission, PreToolUse hooks,
1418
+ * tool execution, PostToolUse hooks, and error handling.
1419
+ */
1420
+ private async executeToolCall(
1421
+ functionToolCall: ChatCompletionMessageFunctionToolCall,
1422
+ abortController: AbortController,
1423
+ toolAbortController: AbortController,
1424
+ finishReason?: string | null,
1425
+ ): Promise<void> {
1426
+ const toolId = functionToolCall.id || "";
1427
+
1428
+ // Check if already interrupted, skip tool execution if so
1429
+ if (abortController.signal.aborted || toolAbortController.signal.aborted) {
1430
+ return;
1431
+ }
1432
+
1433
+ const toolName = functionToolCall.function?.name || "";
1434
+ // Safely parse tool parameters, handle tools without parameters
1435
+ let toolArgs: Record<string, unknown> = {};
1436
+ let jsonRecovered = false;
1437
+ const argsString = functionToolCall.function?.arguments?.trim();
1438
+
1439
+ if (!argsString || argsString === "") {
1440
+ // Tool without parameters, use empty object
1441
+ toolArgs = {};
1442
+ } else {
1443
+ let recoveredArgs = argsString;
1444
+ try {
1445
+ toolArgs = JSON.parse(argsString);
1446
+ } catch {
1447
+ // Attempt to recover truncated JSON (e.g., missing closing braces)
1448
+ recoveredArgs = recoverTruncatedJson(argsString);
1449
+ try {
1450
+ toolArgs = JSON.parse(recoveredArgs);
1451
+ jsonRecovered = true;
1452
+ logger.warn(`Recovered truncated JSON for tool "${toolName}"`);
1453
+ } catch (parseError) {
1454
+ let errorMessage = `Failed to parse tool arguments`;
1455
+ if (finishReason === "length") {
1456
+ errorMessage += " (output truncated, please reduce your output)";
1457
+ }
1458
+ logger?.error(errorMessage, parseError);
1459
+ this.messageManager.updateToolBlock({
1460
+ id: toolId,
1461
+ parameters: argsString,
1462
+ result: errorMessage,
1463
+ success: false,
1464
+ error: errorMessage,
1465
+ stage: "end",
1466
+ name: toolName,
1467
+ compactParams: "",
1468
+ timestamp: Date.now(),
1469
+ });
1470
+ return;
1471
+ }
1472
+ }
1473
+ }
1474
+
1475
+ const compactParams = this.generateCompactParams(toolName, toolArgs);
1476
+
1477
+ // Emit start stage for non-streaming tool calls
1478
+ if (!this.stream) {
1479
+ this.messageManager.updateToolBlock({
1480
+ id: toolId,
1481
+ stage: "start",
1482
+ name: toolName,
1483
+ compactParams,
1484
+ parameters: argsString,
1485
+ });
1486
+ }
1487
+
1488
+ // Emit running stage (tool execution about to start)
1489
+ this.messageManager.updateToolBlock({
1490
+ id: toolId,
1491
+ stage: "running",
1492
+ name: toolName,
1493
+ compactParams,
1494
+ parameters: argsString,
1495
+ parametersChunk: "",
1496
+ });
1497
+
1498
+ try {
1499
+ // Execute PreToolUse hooks before tool execution
1500
+ const shouldExecuteTool = await this.executePreToolUseHooks(
1501
+ toolName,
1502
+ toolArgs,
1503
+ toolId,
1504
+ );
1505
+
1506
+ // If PreToolUse hooks blocked execution, skip tool execution
1507
+ if (!shouldExecuteTool) {
1508
+ logger?.info(`Tool ${toolName} execution blocked by PreToolUse hooks`);
1509
+ return;
1510
+ }
1511
+
1512
+ // Create tool execution context
1513
+ const context: ToolContext = {
1514
+ abortSignal: toolAbortController.signal,
1515
+ backgroundTaskManager: this.backgroundTaskManager,
1516
+ workdir: this.getWorkdir(),
1517
+ originalWorkdir: this.originalWorkdir,
1518
+ messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
1519
+ sessionId: this.messageManager.getSessionId(),
1520
+ toolCallId: toolId,
1521
+ taskManager: this.taskManager,
1522
+ readFileState: this.readFileState,
1523
+ onShortResultUpdate: (shortResult: string) => {
1524
+ this.messageManager.updateToolBlock({
1525
+ id: toolId,
1526
+ shortResult,
1527
+ stage: "running",
1528
+ });
1529
+ },
1530
+ onResultUpdate: (result: string) => {
1531
+ this.messageManager.updateToolBlock({
1532
+ id: toolId,
1533
+ result,
1534
+ stage: "running",
1535
+ });
1536
+ },
1537
+ onCwdChange: async (newCwd: string) => {
1538
+ const oldCwd = this.getWorkdir();
1539
+ this.container.register("Workdir", newCwd);
1540
+ this._onCwdChange?.(newCwd);
1541
+ if (this.hookManager) {
1542
+ const sessionId = this.messageManager.getSessionId();
1543
+ const transcriptPath = this.messageManager.getTranscriptPath();
1544
+ const env = Object.fromEntries(
1545
+ Object.entries(process.env).filter((e) => e[1] !== undefined),
1546
+ ) as Record<string, string>;
1547
+ await this.hookManager.executeCwdChangedHooks(
1548
+ oldCwd,
1549
+ newCwd,
1550
+ sessionId,
1551
+ transcriptPath,
1552
+ env,
1553
+ );
1554
+ }
1555
+ },
1556
+ };
1557
+
1558
+ // Execute tool
1559
+ const toolResult = await this.toolManager.execute(
1560
+ functionToolCall.function?.name || "",
1561
+ toolArgs,
1562
+ context,
1563
+ );
1564
+
1565
+ // Build result content, adding truncation warning if JSON was recovered
1566
+ let toolResultContent =
1567
+ toolResult.content ||
1568
+ (toolResult.error ? `Error: ${toolResult.error}` : "");
1569
+ if (jsonRecovered) {
1570
+ toolResultContent +=
1571
+ "\n\nTool arguments were truncated (likely exceeded max output tokens). Please reduce your output or split into multiple tool calls.";
1572
+ }
1573
+
1574
+ // Update message state - tool execution completed
1575
+ this.messageManager.updateToolBlock({
1576
+ id: toolId,
1577
+ parameters: argsString,
1578
+ result: toolResultContent,
1579
+ success: toolResult.success,
1580
+ error: toolResult.error,
1581
+ stage: "end",
1582
+ name: toolName,
1583
+ compactParams,
1584
+ shortResult: toolResult.shortResult,
1585
+ isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
1586
+ startLineNumber: toolResult.startLineNumber,
1587
+ timestamp: Date.now(),
1588
+ });
1589
+
1590
+ // Execute PostToolUse hooks after successful tool completion
1591
+ await this.executePostToolUseHooks(
1592
+ toolId,
1593
+ toolName,
1594
+ toolArgs,
1595
+ toolResult,
1596
+ );
1597
+ } catch (toolError) {
1598
+ const errorMessage =
1599
+ toolError instanceof Error ? toolError.message : String(toolError);
1600
+
1601
+ this.messageManager.updateToolBlock({
1602
+ id: toolId,
1603
+ parameters: JSON.stringify(toolArgs, null, 2),
1604
+ result: `Tool execution failed: ${errorMessage}`,
1605
+ success: false,
1606
+ error: errorMessage,
1607
+ stage: "end",
1608
+ name: toolName,
1609
+ compactParams,
1610
+ isManuallyBackgrounded: false,
1611
+ timestamp: Date.now(),
1612
+ });
1613
+ }
1614
+ }
1615
+
1587
1616
  /**
1588
1617
  * Execute PreToolUse hooks before tool execution
1589
1618
  * Returns true if hooks allow tool execution, false if blocked
@@ -1682,9 +1711,9 @@ export class AIManager {
1682
1711
  toolResponse,
1683
1712
  subagentType: this.subagentType, // Include subagent type in hook context
1684
1713
  planFilePath: this.permissionManager?.getPlanFilePath(),
1685
- env:
1686
- this.container.get<Record<string, string>>("MergedEnv") ||
1687
- (process.env as Record<string, string>),
1714
+ env: Object.fromEntries(
1715
+ Object.entries(process.env).filter((e) => e[1] !== undefined),
1716
+ ) as Record<string, string>, // Include environment variables
1688
1717
  };
1689
1718
 
1690
1719
  const results = await this.hookManager.executeHooks(
@@ -68,7 +68,7 @@ export class BackgroundTaskManager {
68
68
  stdio: "pipe",
69
69
  detached: true,
70
70
  cwd: this.workdir,
71
- env: this.container.get<Record<string, string>>("MergedEnv") || {
71
+ env: {
72
72
  ...process.env,
73
73
  },
74
74
  });
@@ -48,7 +48,7 @@ export class BangManager {
48
48
  shell: true,
49
49
  stdio: "pipe",
50
50
  cwd: this.workdir,
51
- env: this.container.get<Record<string, string>>("MergedEnv") || {
51
+ env: {
52
52
  ...process.env,
53
53
  },
54
54
  });