wave-agent-sdk 0.17.9 → 0.17.11

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 (85) hide show
  1. package/builtin/skills/settings/ENV.md +1 -1
  2. package/dist/managers/aiManager.d.ts +11 -4
  3. package/dist/managers/aiManager.d.ts.map +1 -1
  4. package/dist/managers/aiManager.js +232 -200
  5. package/dist/managers/backgroundTaskManager.js +1 -1
  6. package/dist/managers/bangManager.js +1 -1
  7. package/dist/managers/hookManager.js +2 -4
  8. package/dist/managers/mcpManager.d.ts.map +1 -1
  9. package/dist/managers/mcpManager.js +1 -3
  10. package/dist/managers/messageManager.d.ts.map +1 -1
  11. package/dist/managers/messageManager.js +1 -8
  12. package/dist/managers/toolManager.d.ts +6 -0
  13. package/dist/managers/toolManager.d.ts.map +1 -1
  14. package/dist/managers/toolManager.js +17 -12
  15. package/dist/services/aiService.d.ts.map +1 -1
  16. package/dist/services/aiService.js +20 -12
  17. package/dist/services/authService.d.ts +0 -1
  18. package/dist/services/authService.d.ts.map +1 -1
  19. package/dist/services/authService.js +1 -29
  20. package/dist/services/configurationService.d.ts.map +1 -1
  21. package/dist/services/configurationService.js +13 -0
  22. package/dist/services/fileWatcher.d.ts +2 -0
  23. package/dist/services/fileWatcher.d.ts.map +1 -1
  24. package/dist/services/fileWatcher.js +12 -1
  25. package/dist/services/jsonlHandler.d.ts +1 -1
  26. package/dist/services/jsonlHandler.d.ts.map +1 -1
  27. package/dist/services/jsonlHandler.js +7 -22
  28. package/dist/services/session.d.ts +2 -9
  29. package/dist/services/session.d.ts.map +1 -1
  30. package/dist/services/session.js +16 -46
  31. package/dist/tools/bashTool.d.ts.map +1 -1
  32. package/dist/tools/bashTool.js +3 -1
  33. package/dist/tools/editTool.d.ts.map +1 -1
  34. package/dist/tools/editTool.js +28 -1
  35. package/dist/tools/enterPlanMode.d.ts.map +1 -1
  36. package/dist/tools/enterPlanMode.js +8 -0
  37. package/dist/tools/exitPlanMode.d.ts.map +1 -1
  38. package/dist/tools/exitPlanMode.js +8 -0
  39. package/dist/tools/readTool.d.ts.map +1 -1
  40. package/dist/tools/readTool.js +18 -10
  41. package/dist/tools/types.d.ts +8 -2
  42. package/dist/tools/types.d.ts.map +1 -1
  43. package/dist/tools/writeTool.d.ts.map +1 -1
  44. package/dist/tools/writeTool.js +14 -1
  45. package/dist/types/agent.d.ts +0 -2
  46. package/dist/types/agent.d.ts.map +1 -1
  47. package/dist/types/config.d.ts +1 -0
  48. package/dist/types/config.d.ts.map +1 -1
  49. package/dist/utils/cacheControlUtils.d.ts +20 -15
  50. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  51. package/dist/utils/cacheControlUtils.js +69 -66
  52. package/dist/utils/constants.d.ts +1 -1
  53. package/dist/utils/constants.js +1 -1
  54. package/dist/utils/containerSetup.d.ts.map +1 -1
  55. package/dist/utils/containerSetup.js +1 -6
  56. package/dist/utils/taskReminder.d.ts +17 -0
  57. package/dist/utils/taskReminder.d.ts.map +1 -0
  58. package/dist/utils/taskReminder.js +76 -0
  59. package/package.json +1 -1
  60. package/src/managers/aiManager.ts +326 -262
  61. package/src/managers/backgroundTaskManager.ts +1 -1
  62. package/src/managers/bangManager.ts +1 -1
  63. package/src/managers/hookManager.ts +6 -6
  64. package/src/managers/mcpManager.ts +2 -4
  65. package/src/managers/messageManager.ts +1 -9
  66. package/src/managers/toolManager.ts +18 -12
  67. package/src/services/aiService.ts +23 -12
  68. package/src/services/authService.ts +1 -31
  69. package/src/services/configurationService.ts +21 -0
  70. package/src/services/fileWatcher.ts +15 -1
  71. package/src/services/jsonlHandler.ts +7 -27
  72. package/src/services/session.ts +16 -57
  73. package/src/tools/bashTool.ts +3 -1
  74. package/src/tools/editTool.ts +31 -1
  75. package/src/tools/enterPlanMode.ts +9 -0
  76. package/src/tools/exitPlanMode.ts +10 -0
  77. package/src/tools/readTool.ts +20 -10
  78. package/src/tools/types.ts +15 -3
  79. package/src/tools/writeTool.ts +15 -1
  80. package/src/types/agent.ts +0 -2
  81. package/src/types/config.ts +1 -0
  82. package/src/utils/cacheControlUtils.ts +77 -95
  83. package/src/utils/constants.ts +1 -1
  84. package/src/utils/containerSetup.ts +3 -7
  85. package/src/utils/taskReminder.ts +108 -0
@@ -27,7 +27,7 @@ Wave uses several environment variables to control its core functionality.
27
27
  | `WAVE_CUSTOM_HEADERS` | Custom HTTP headers for the AI gateway. Newline-separated `Key: Value` pairs (e.g., `"X-Foo: bar\nAuthorization: Bearer xxx"`). | - |
28
28
  | `WAVE_MODEL` | The primary AI model to use for the agent. | `gemini-3-flash` |
29
29
  | `WAVE_FAST_MODEL` | The fast AI model to use for quick tasks. | `gemini-2.5-flash` |
30
- | `WAVE_MAX_INPUT_TOKENS` | Maximum number of input tokens allowed. | `128000` |
30
+ | `WAVE_MAX_INPUT_TOKENS` | Maximum number of input tokens allowed. | `200000` |
31
31
  | `WAVE_MAX_OUTPUT_TOKENS` | Maximum number of output tokens allowed. | `16384` |
32
32
  | `WAVE_DISABLE_AUTO_MEMORY` | Set to `1` or `true` to disable the auto-memory feature. | `false` |
33
33
  | `WAVE_AUTO_MEMORY_FREQUENCY` | Auto memory update frequency. `1` = every turn, `2` = every 2 turns, etc. | `1` |
@@ -31,6 +31,8 @@ export declare class AIManager {
31
31
  private originalWorkdir;
32
32
  private consecutiveCompactionFailures;
33
33
  private readonly maxTurns?;
34
+ /** Tracks file mtime/hash at read time for staleness detection on Edit/Write */
35
+ private readFileState;
34
36
  /** Override tool_choice for this AI manager (e.g. for structured output) */
35
37
  toolChoiceOverride?: "auto" | "none" | "required" | {
36
38
  type: "function";
@@ -69,11 +71,11 @@ export declare class AIManager {
69
71
  */
70
72
  private getFilteredToolsConfig;
71
73
  /**
72
- * Build plan mode system-reminder messages to inject into the API message stream.
73
- * These are transient messages not stored in the message history.
74
- * This preserves prompt caching by keeping the system prompt constant.
74
+ * Add plan mode reminder as a persistent meta message to the message manager.
75
+ * Called before getMessages() so the stored message is included in the API call.
75
76
  */
76
- private buildPlanModeMessages;
77
+ private maybeAddPlanModeMessage;
78
+ private maybeInjectTaskReminderIntoMessages;
77
79
  setIsLoading(isLoading: boolean): void;
78
80
  abortAIMessage(): void;
79
81
  private generateCompactParams;
@@ -108,6 +110,11 @@ export declare class AIManager {
108
110
  * @returns Promise<boolean> - true if should continue conversation, false if should stop
109
111
  */
110
112
  private executeStopHooks;
113
+ /**
114
+ * Execute a single tool call: arg parsing, block emission, PreToolUse hooks,
115
+ * tool execution, PostToolUse hooks, and error handling.
116
+ */
117
+ private executeToolCall;
111
118
  /**
112
119
  * Execute PreToolUse hooks before tool execution
113
120
  * Returns true if hooks allow tool execution, false if blocked
@@ -1 +1 @@
1
- {"version":3,"file":"aiManager.d.ts","sourceRoot":"","sources":["../../src/managers/aiManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"aiManager.d.ts","sourceRoot":"","sources":["../../src/managers/aiManager.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@ import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
3
3
  import { microcompactMessages } from "../utils/microcompact.js";
4
4
  import { parseTaskNotificationXml } from "../utils/notificationXml.js";
5
5
  import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
6
- import * as fs from "node:fs/promises";
6
+ import { getTaskReminderTurnCounts, maybeInjectTaskReminder, TASK_REMINDER_CONFIG, } from "../utils/taskReminder.js";
7
7
  import { existsSync } from "node:fs";
8
8
  import { buildSystemPrompt } from "../prompts/index.js";
9
9
  import { buildPlanModeReminder, buildPlanModeReEntryReminder, buildExitedPlanModeReminder, } from "../prompts/planModeReminders.js";
@@ -19,6 +19,8 @@ export class AIManager {
19
19
  this.abortController = null;
20
20
  this.toolAbortController = null;
21
21
  this.consecutiveCompactionFailures = 0;
22
+ /** Tracks file mtime/hash at read time for staleness detection on Edit/Write */
23
+ this.readFileState = new Map();
22
24
  this.isCompacting = false;
23
25
  this.systemPrompt = options.systemPrompt;
24
26
  this.subagentType = options.subagentType; // Store subagent type
@@ -122,50 +124,62 @@ export class AIManager {
122
124
  });
123
125
  }
124
126
  /**
125
- * Build plan mode system-reminder messages to inject into the API message stream.
126
- * These are transient messages not stored in the message history.
127
- * This preserves prompt caching by keeping the system prompt constant.
127
+ * Add plan mode reminder as a persistent meta message to the message manager.
128
+ * Called before getMessages() so the stored message is included in the API call.
128
129
  */
129
- buildPlanModeMessages(currentMode) {
130
- const messages = [];
130
+ maybeAddPlanModeMessage(currentMode) {
131
131
  if (!this.permissionManager)
132
- return messages;
132
+ return;
133
133
  // Handle exit notification (one-time after leaving plan mode)
134
134
  if (this.permissionManager.getNeedsPlanModeExitAttachment()) {
135
135
  const planFilePath = this.permissionManager.getPlanFilePath();
136
136
  const planExists = planFilePath ? existsSync(planFilePath) : false;
137
- messages.push({
138
- role: "user",
137
+ this.messageManager.addUserMessage({
139
138
  content: buildExitedPlanModeReminder(planFilePath, planExists),
139
+ isMeta: true,
140
140
  });
141
141
  this.permissionManager.setNeedsPlanModeExitAttachment(false);
142
142
  }
143
143
  if (currentMode !== "plan")
144
- return messages;
144
+ return;
145
145
  const planFilePath = this.permissionManager.getPlanFilePath();
146
146
  if (!planFilePath)
147
- return messages;
147
+ return;
148
148
  const planExists = existsSync(planFilePath);
149
149
  // One-time plan entry reminder
150
150
  const planMgr = this.planManager;
151
151
  if (planMgr?.isPlanEntryReminderPending()) {
152
152
  if (this.permissionManager.hasExitedPlanModeInSession() && planExists) {
153
153
  // Re-entry: use small reminder
154
- messages.push({
155
- role: "user",
154
+ this.messageManager.addUserMessage({
156
155
  content: buildPlanModeReEntryReminder(planFilePath),
156
+ isMeta: true,
157
157
  });
158
158
  }
159
159
  else {
160
160
  // First entry: use full reminder
161
- messages.push({
162
- role: "user",
161
+ this.messageManager.addUserMessage({
163
162
  content: buildPlanModeReminder(planFilePath, planExists, !!this.subagentType),
163
+ isMeta: true,
164
164
  });
165
165
  }
166
166
  planMgr.consumePlanEntryReminder();
167
167
  }
168
- return messages;
168
+ }
169
+ async maybeInjectTaskReminderIntoMessages(messages, toolNames) {
170
+ // Guard: no task tools available
171
+ if (!toolNames.has("TaskUpdate"))
172
+ return;
173
+ const internalMessages = this.messageManager.getMessages();
174
+ const turnCounts = getTaskReminderTurnCounts(internalMessages);
175
+ if (turnCounts.turnsSinceLastTaskManagement <
176
+ TASK_REMINDER_CONFIG.TURNS_SINCE_WRITE ||
177
+ turnCounts.turnsSinceLastReminder <
178
+ TASK_REMINDER_CONFIG.TURNS_BETWEEN_REMINDERS) {
179
+ return;
180
+ }
181
+ const tasks = await this.taskManager.listTasks();
182
+ maybeInjectTaskReminder(messages, turnCounts, tasks);
169
183
  }
170
184
  setIsLoading(isLoading) {
171
185
  this.isLoading = isLoading;
@@ -299,6 +313,18 @@ export class AIManager {
299
313
  const enhancedSummary = await this.buildPostCompactContext(compactResult.content);
300
314
  // 7. Execute message reconstruction
301
315
  this.messageManager.compactMessagesAndUpdateSession(enhancedSummary, compactUsage);
316
+ // Re-add plan mode reminder as persistent meta message after compaction
317
+ const postCompactMode = this.permissionManager?.getCurrentEffectiveMode(this.getModelConfig().permissionMode);
318
+ if (postCompactMode === "plan") {
319
+ const planFilePath = this.permissionManager?.getPlanFilePath();
320
+ if (planFilePath) {
321
+ const planExists = existsSync(planFilePath);
322
+ this.messageManager.addUserMessage({
323
+ content: buildPlanModeReminder(planFilePath, planExists, !!this.subagentType),
324
+ isMeta: true,
325
+ });
326
+ }
327
+ }
302
328
  // 8. Track usage
303
329
  if (compactUsage && this.callbacks?.onUsageAdded) {
304
330
  this.callbacks.onUsageAdded(compactUsage);
@@ -376,22 +402,6 @@ export class AIManager {
376
402
  if (usedTokens >= POST_COMPACT_TOKEN_BUDGET)
377
403
  break;
378
404
  }
379
- // 2. Plan mode context
380
- const currentMode = this.permissionManager?.getCurrentEffectiveMode(this.getModelConfig().permissionMode);
381
- if (currentMode === "plan") {
382
- const planFilePath = this.permissionManager?.getPlanFilePath();
383
- if (planFilePath) {
384
- let planExists = false;
385
- try {
386
- await fs.access(planFilePath);
387
- planExists = true;
388
- }
389
- catch {
390
- // Plan file doesn't exist yet
391
- }
392
- contextParts.push(`\n\n${buildPlanModeReminder(planFilePath, planExists, !!this.subagentType)}`);
393
- }
394
- }
395
405
  // 4. Invoked skills context (with token budget, matching Claude Code)
396
406
  const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25000;
397
407
  const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5000;
@@ -549,6 +559,10 @@ export class AIManager {
549
559
  abortController = this.abortController;
550
560
  toolAbortController = this.toolAbortController;
551
561
  }
562
+ // Get current permission mode
563
+ const currentMode = this.permissionManager?.getCurrentEffectiveMode(this.getModelConfig().permissionMode);
564
+ // Add plan mode reminder as persistent meta message before getting messages
565
+ this.maybeAddPlanModeMessage(currentMode);
552
566
  // Get recent message history with microcompact applied
553
567
  const rawMessages = this.messageManager.getMessages();
554
568
  const microcompactedMessages = microcompactMessages(rawMessages, {
@@ -562,19 +576,11 @@ export class AIManager {
562
576
  // Track if assistant message has been created
563
577
  let assistantMessageCreated = false;
564
578
  logger?.debug("modelConfig in sendAIMessage", this.getModelConfig());
565
- // Get current permission mode and plan file path
566
- const currentMode = this.permissionManager?.getCurrentEffectiveMode(this.getModelConfig().permissionMode);
567
579
  const toolsConfig = this.getFilteredToolsConfig();
568
580
  const toolNames = new Set(toolsConfig.map((t) => t.function.name));
569
581
  const filteredToolPlugins = this.toolManager
570
582
  .getTools()
571
583
  .filter((t) => toolNames.has(t.name));
572
- // Inject plan mode system-reminder messages (not system prompt)
573
- // This preserves prompt caching by keeping the system prompt constant
574
- const planModeMessages = this.buildPlanModeMessages(currentMode);
575
- if (planModeMessages.length > 0) {
576
- recentMessages.push(...planModeMessages);
577
- }
578
584
  let autoMemoryOptions;
579
585
  if (this.getAutoMemoryEnabled()) {
580
586
  const directory = this.memoryService.getAutoMemoryDirectory(this.getWorkdir());
@@ -603,6 +609,8 @@ export class AIManager {
603
609
  maxTokens: maxTokens, // Pass max tokens override
604
610
  toolChoice: this.toolChoiceOverride, // Pass tool_choice override
605
611
  };
612
+ // Inject task reminder if needed (not persisted, regenerated each turn)
613
+ await this.maybeInjectTaskReminderIntoMessages(callAgentOptions.messages, toolNames);
606
614
  // Add streaming callbacks only if streaming is enabled
607
615
  if (this.stream) {
608
616
  callAgentOptions.onContentUpdate = (content) => {
@@ -719,170 +727,34 @@ export class AIManager {
719
727
  }
720
728
  }
721
729
  if (toolCalls.length > 0) {
722
- // Execute all tools in parallel using Promise.all
723
- const toolExecutionPromises = toolCalls.map(async (functionToolCall) => {
724
- const toolId = functionToolCall.id || "";
725
- // Check if already interrupted, skip tool execution if so
726
- if (abortController.signal.aborted ||
727
- toolAbortController.signal.aborted) {
728
- return;
729
- }
730
- const toolName = functionToolCall.function?.name || "";
731
- // Safely parse tool parameters, handle tools without parameters
732
- let toolArgs = {};
733
- let jsonRecovered = false;
734
- const argsString = functionToolCall.function?.arguments?.trim();
735
- if (!argsString || argsString === "") {
736
- // Tool without parameters, use empty object
737
- toolArgs = {};
730
+ // Partition tool calls into batches: consecutive concurrency-safe
731
+ // tools run in parallel, non-safe tools (Edit, Write, MCP) run one
732
+ // at a time to prevent read-modify-write races on the same file.
733
+ const batches = [];
734
+ for (const call of toolCalls) {
735
+ const toolName = call.function?.name || "";
736
+ const safe = this.toolManager.isConcurrencySafe(toolName);
737
+ const lastBatch = batches[batches.length - 1];
738
+ if (lastBatch && lastBatch.safe && safe) {
739
+ lastBatch.calls.push(call);
738
740
  }
739
741
  else {
740
- let recoveredArgs = argsString;
741
- try {
742
- toolArgs = JSON.parse(argsString);
743
- }
744
- catch {
745
- // Attempt to recover truncated JSON (e.g., missing closing braces)
746
- recoveredArgs = recoverTruncatedJson(argsString);
747
- try {
748
- toolArgs = JSON.parse(recoveredArgs);
749
- jsonRecovered = true;
750
- logger.warn(`Recovered truncated JSON for tool "${toolName}"`);
751
- }
752
- catch (parseError) {
753
- let errorMessage = `Failed to parse tool arguments`;
754
- if (result.finish_reason === "length") {
755
- errorMessage +=
756
- " (output truncated, please reduce your output)";
757
- }
758
- logger?.error(errorMessage, parseError);
759
- this.messageManager.updateToolBlock({
760
- id: toolId,
761
- parameters: argsString,
762
- result: errorMessage,
763
- success: false,
764
- error: errorMessage,
765
- stage: "end",
766
- name: toolName,
767
- compactParams: "",
768
- timestamp: Date.now(),
769
- });
770
- return;
771
- }
772
- }
742
+ batches.push({ calls: [call], safe });
773
743
  }
774
- const compactParams = this.generateCompactParams(toolName, toolArgs);
775
- // Emit start stage for non-streaming tool calls
776
- if (!this.stream) {
777
- this.messageManager.updateToolBlock({
778
- id: toolId,
779
- stage: "start",
780
- name: toolName,
781
- compactParams,
782
- parameters: argsString,
783
- });
744
+ }
745
+ // Execute batches sequentially; within a safe batch, tools run in parallel
746
+ for (const batch of batches) {
747
+ if (abortController.signal.aborted ||
748
+ toolAbortController.signal.aborted) {
749
+ break;
784
750
  }
785
- // Emit running stage (tool execution about to start)
786
- this.messageManager.updateToolBlock({
787
- id: toolId,
788
- stage: "running",
789
- name: toolName,
790
- compactParams,
791
- parameters: argsString,
792
- parametersChunk: "",
793
- });
794
- try {
795
- // Execute PreToolUse hooks before tool execution
796
- const shouldExecuteTool = await this.executePreToolUseHooks(toolName, toolArgs, toolId);
797
- // If PreToolUse hooks blocked execution, skip tool execution
798
- if (!shouldExecuteTool) {
799
- logger?.info(`Tool ${toolName} execution blocked by PreToolUse hooks`);
800
- return; // Skip this tool and return from this map function
801
- }
802
- // Create tool execution context
803
- const context = {
804
- abortSignal: toolAbortController.signal,
805
- backgroundTaskManager: this.backgroundTaskManager,
806
- workdir: this.getWorkdir(),
807
- originalWorkdir: this.originalWorkdir,
808
- env: this.container.get("MergedEnv"),
809
- messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
810
- sessionId: this.messageManager.getSessionId(),
811
- toolCallId: toolId,
812
- taskManager: this.taskManager,
813
- onShortResultUpdate: (shortResult) => {
814
- this.messageManager.updateToolBlock({
815
- id: toolId,
816
- shortResult,
817
- stage: "running", // Keep it in running stage while updating shortResult
818
- });
819
- },
820
- onResultUpdate: (result) => {
821
- this.messageManager.updateToolBlock({
822
- id: toolId,
823
- result,
824
- stage: "running", // Keep it in running stage while updating result
825
- });
826
- },
827
- onCwdChange: async (newCwd) => {
828
- const oldCwd = this.getWorkdir();
829
- this.container.register("Workdir", newCwd);
830
- this._onCwdChange?.(newCwd);
831
- if (this.hookManager) {
832
- const sessionId = this.messageManager.getSessionId();
833
- const transcriptPath = this.messageManager.getTranscriptPath();
834
- const env = Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined));
835
- await this.hookManager.executeCwdChangedHooks(oldCwd, newCwd, sessionId, transcriptPath, env);
836
- }
837
- },
838
- };
839
- // Execute tool
840
- const toolResult = await this.toolManager.execute(functionToolCall.function?.name || "", toolArgs, context);
841
- // Build result content, adding truncation warning if JSON was recovered
842
- let toolResultContent = toolResult.content ||
843
- (toolResult.error ? `Error: ${toolResult.error}` : "");
844
- if (jsonRecovered) {
845
- toolResultContent +=
846
- "\n\nTool arguments were truncated (likely exceeded max output tokens). Please reduce your output or split into multiple tool calls.";
847
- }
848
- // Update message state - tool execution completed
849
- this.messageManager.updateToolBlock({
850
- id: toolId,
851
- parameters: argsString,
852
- result: toolResultContent,
853
- success: toolResult.success,
854
- error: toolResult.error,
855
- stage: "end",
856
- name: toolName,
857
- compactParams,
858
- shortResult: toolResult.shortResult,
859
- isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
860
- startLineNumber: toolResult.startLineNumber,
861
- timestamp: Date.now(),
862
- });
863
- // Execute PostToolUse hooks after successful tool completion
864
- await this.executePostToolUseHooks(toolId, toolName, toolArgs, toolResult);
751
+ if (batch.calls.length === 1) {
752
+ await this.executeToolCall(batch.calls[0], abortController, toolAbortController, result.finish_reason);
865
753
  }
866
- catch (toolError) {
867
- const errorMessage = toolError instanceof Error
868
- ? toolError.message
869
- : String(toolError);
870
- this.messageManager.updateToolBlock({
871
- id: toolId,
872
- parameters: JSON.stringify(toolArgs, null, 2),
873
- result: `Tool execution failed: ${errorMessage}`,
874
- success: false,
875
- error: errorMessage,
876
- stage: "end",
877
- name: toolName,
878
- compactParams,
879
- isManuallyBackgrounded: false,
880
- timestamp: Date.now(),
881
- });
754
+ else {
755
+ await Promise.all(batch.calls.map((call) => this.executeToolCall(call, abortController, toolAbortController, result.finish_reason)));
882
756
  }
883
- });
884
- // Wait for all tools to complete execution in parallel
885
- await Promise.all(toolExecutionPromises);
757
+ }
886
758
  }
887
759
  // Handle token statistics and message compaction
888
760
  await this.handleTokenUsageAndCompaction(result.usage, abortController);
@@ -1172,6 +1044,167 @@ export class AIManager {
1172
1044
  return false;
1173
1045
  }
1174
1046
  }
1047
+ /**
1048
+ * Execute a single tool call: arg parsing, block emission, PreToolUse hooks,
1049
+ * tool execution, PostToolUse hooks, and error handling.
1050
+ */
1051
+ async executeToolCall(functionToolCall, abortController, toolAbortController, finishReason) {
1052
+ const toolId = functionToolCall.id || "";
1053
+ // Check if already interrupted, skip tool execution if so
1054
+ if (abortController.signal.aborted || toolAbortController.signal.aborted) {
1055
+ return;
1056
+ }
1057
+ const toolName = functionToolCall.function?.name || "";
1058
+ // Safely parse tool parameters, handle tools without parameters
1059
+ let toolArgs = {};
1060
+ let jsonRecovered = false;
1061
+ const argsString = functionToolCall.function?.arguments?.trim();
1062
+ if (!argsString || argsString === "") {
1063
+ // Tool without parameters, use empty object
1064
+ toolArgs = {};
1065
+ }
1066
+ else {
1067
+ let recoveredArgs = argsString;
1068
+ try {
1069
+ toolArgs = JSON.parse(argsString);
1070
+ }
1071
+ catch {
1072
+ // Attempt to recover truncated JSON (e.g., missing closing braces)
1073
+ recoveredArgs = recoverTruncatedJson(argsString);
1074
+ try {
1075
+ toolArgs = JSON.parse(recoveredArgs);
1076
+ jsonRecovered = true;
1077
+ logger.warn(`Recovered truncated JSON for tool "${toolName}"`);
1078
+ }
1079
+ catch (parseError) {
1080
+ let errorMessage = `Failed to parse tool arguments`;
1081
+ if (finishReason === "length") {
1082
+ errorMessage += " (output truncated, please reduce your output)";
1083
+ }
1084
+ logger?.error(errorMessage, parseError);
1085
+ this.messageManager.updateToolBlock({
1086
+ id: toolId,
1087
+ parameters: argsString,
1088
+ result: errorMessage,
1089
+ success: false,
1090
+ error: errorMessage,
1091
+ stage: "end",
1092
+ name: toolName,
1093
+ compactParams: "",
1094
+ timestamp: Date.now(),
1095
+ });
1096
+ return;
1097
+ }
1098
+ }
1099
+ }
1100
+ const compactParams = this.generateCompactParams(toolName, toolArgs);
1101
+ // Emit start stage for non-streaming tool calls
1102
+ if (!this.stream) {
1103
+ this.messageManager.updateToolBlock({
1104
+ id: toolId,
1105
+ stage: "start",
1106
+ name: toolName,
1107
+ compactParams,
1108
+ parameters: argsString,
1109
+ });
1110
+ }
1111
+ // Emit running stage (tool execution about to start)
1112
+ this.messageManager.updateToolBlock({
1113
+ id: toolId,
1114
+ stage: "running",
1115
+ name: toolName,
1116
+ compactParams,
1117
+ parameters: argsString,
1118
+ parametersChunk: "",
1119
+ });
1120
+ try {
1121
+ // Execute PreToolUse hooks before tool execution
1122
+ const shouldExecuteTool = await this.executePreToolUseHooks(toolName, toolArgs, toolId);
1123
+ // If PreToolUse hooks blocked execution, skip tool execution
1124
+ if (!shouldExecuteTool) {
1125
+ logger?.info(`Tool ${toolName} execution blocked by PreToolUse hooks`);
1126
+ return;
1127
+ }
1128
+ // Create tool execution context
1129
+ const context = {
1130
+ abortSignal: toolAbortController.signal,
1131
+ backgroundTaskManager: this.backgroundTaskManager,
1132
+ workdir: this.getWorkdir(),
1133
+ originalWorkdir: this.originalWorkdir,
1134
+ messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
1135
+ sessionId: this.messageManager.getSessionId(),
1136
+ toolCallId: toolId,
1137
+ taskManager: this.taskManager,
1138
+ readFileState: this.readFileState,
1139
+ onShortResultUpdate: (shortResult) => {
1140
+ this.messageManager.updateToolBlock({
1141
+ id: toolId,
1142
+ shortResult,
1143
+ stage: "running",
1144
+ });
1145
+ },
1146
+ onResultUpdate: (result) => {
1147
+ this.messageManager.updateToolBlock({
1148
+ id: toolId,
1149
+ result,
1150
+ stage: "running",
1151
+ });
1152
+ },
1153
+ onCwdChange: async (newCwd) => {
1154
+ const oldCwd = this.getWorkdir();
1155
+ this.container.register("Workdir", newCwd);
1156
+ this._onCwdChange?.(newCwd);
1157
+ if (this.hookManager) {
1158
+ const sessionId = this.messageManager.getSessionId();
1159
+ const transcriptPath = this.messageManager.getTranscriptPath();
1160
+ const env = Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined));
1161
+ await this.hookManager.executeCwdChangedHooks(oldCwd, newCwd, sessionId, transcriptPath, env);
1162
+ }
1163
+ },
1164
+ };
1165
+ // Execute tool
1166
+ const toolResult = await this.toolManager.execute(functionToolCall.function?.name || "", toolArgs, context);
1167
+ // Build result content, adding truncation warning if JSON was recovered
1168
+ let toolResultContent = toolResult.content ||
1169
+ (toolResult.error ? `Error: ${toolResult.error}` : "");
1170
+ if (jsonRecovered) {
1171
+ toolResultContent +=
1172
+ "\n\nTool arguments were truncated (likely exceeded max output tokens). Please reduce your output or split into multiple tool calls.";
1173
+ }
1174
+ // Update message state - tool execution completed
1175
+ this.messageManager.updateToolBlock({
1176
+ id: toolId,
1177
+ parameters: argsString,
1178
+ result: toolResultContent,
1179
+ success: toolResult.success,
1180
+ error: toolResult.error,
1181
+ stage: "end",
1182
+ name: toolName,
1183
+ compactParams,
1184
+ shortResult: toolResult.shortResult,
1185
+ isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
1186
+ startLineNumber: toolResult.startLineNumber,
1187
+ timestamp: Date.now(),
1188
+ });
1189
+ // Execute PostToolUse hooks after successful tool completion
1190
+ await this.executePostToolUseHooks(toolId, toolName, toolArgs, toolResult);
1191
+ }
1192
+ catch (toolError) {
1193
+ const errorMessage = toolError instanceof Error ? toolError.message : String(toolError);
1194
+ this.messageManager.updateToolBlock({
1195
+ id: toolId,
1196
+ parameters: JSON.stringify(toolArgs, null, 2),
1197
+ result: `Tool execution failed: ${errorMessage}`,
1198
+ success: false,
1199
+ error: errorMessage,
1200
+ stage: "end",
1201
+ name: toolName,
1202
+ compactParams,
1203
+ isManuallyBackgrounded: false,
1204
+ timestamp: Date.now(),
1205
+ });
1206
+ }
1207
+ }
1175
1208
  /**
1176
1209
  * Execute PreToolUse hooks before tool execution
1177
1210
  * Returns true if hooks allow tool execution, false if blocked
@@ -1243,8 +1276,7 @@ export class AIManager {
1243
1276
  toolResponse,
1244
1277
  subagentType: this.subagentType, // Include subagent type in hook context
1245
1278
  planFilePath: this.permissionManager?.getPlanFilePath(),
1246
- env: this.container.get("MergedEnv") ||
1247
- process.env,
1279
+ env: Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined)), // Include environment variables
1248
1280
  };
1249
1281
  const results = await this.hookManager.executeHooks("PostToolUse", context);
1250
1282
  // Process hook results to handle exit codes and update tool results
@@ -39,7 +39,7 @@ export class BackgroundTaskManager {
39
39
  stdio: "pipe",
40
40
  detached: true,
41
41
  cwd: this.workdir,
42
- env: this.container.get("MergedEnv") || {
42
+ env: {
43
43
  ...process.env,
44
44
  },
45
45
  });
@@ -25,7 +25,7 @@ export class BangManager {
25
25
  shell: true,
26
26
  stdio: "pipe",
27
27
  cwd: this.workdir,
28
- env: this.container.get("MergedEnv") || {
28
+ env: {
29
29
  ...process.env,
30
30
  },
31
31
  });
@@ -718,8 +718,7 @@ export class HookManager {
718
718
  transcriptPath,
719
719
  cwd: this.workdir,
720
720
  endSource: source,
721
- env: this.container.get("MergedEnv") ||
722
- process.env,
721
+ env: Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined)),
723
722
  };
724
723
  const results = await this.executeHooks("SessionEnd", context);
725
724
  // Process results but never block shutdown
@@ -768,8 +767,7 @@ export class HookManager {
768
767
  transcriptPath,
769
768
  cwd: this.workdir,
770
769
  compactSummary,
771
- env: this.container.get("MergedEnv") ||
772
- process.env,
770
+ env: Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined)),
773
771
  };
774
772
  const results = await this.executeHooks("PostCompact", context);
775
773
  if (results.length > 0) {