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.
- package/builtin/skills/settings/ENV.md +1 -1
- package/dist/managers/aiManager.d.ts +11 -4
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +232 -200
- package/dist/managers/backgroundTaskManager.js +1 -1
- package/dist/managers/bangManager.js +1 -1
- package/dist/managers/hookManager.js +2 -4
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +1 -3
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +1 -8
- package/dist/managers/toolManager.d.ts +6 -0
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +17 -12
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +20 -12
- package/dist/services/authService.d.ts +0 -1
- package/dist/services/authService.d.ts.map +1 -1
- package/dist/services/authService.js +1 -29
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +13 -0
- package/dist/services/fileWatcher.d.ts +2 -0
- package/dist/services/fileWatcher.d.ts.map +1 -1
- package/dist/services/fileWatcher.js +12 -1
- package/dist/services/jsonlHandler.d.ts +1 -1
- package/dist/services/jsonlHandler.d.ts.map +1 -1
- package/dist/services/jsonlHandler.js +7 -22
- package/dist/services/session.d.ts +2 -9
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +16 -46
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +3 -1
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +28 -1
- package/dist/tools/enterPlanMode.d.ts.map +1 -1
- package/dist/tools/enterPlanMode.js +8 -0
- package/dist/tools/exitPlanMode.d.ts.map +1 -1
- package/dist/tools/exitPlanMode.js +8 -0
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +18 -10
- package/dist/tools/types.d.ts +8 -2
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +14 -1
- package/dist/types/agent.d.ts +0 -2
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.d.ts +20 -15
- package/dist/utils/cacheControlUtils.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.js +69 -66
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +1 -6
- package/dist/utils/taskReminder.d.ts +17 -0
- package/dist/utils/taskReminder.d.ts.map +1 -0
- package/dist/utils/taskReminder.js +76 -0
- package/package.json +1 -1
- package/src/managers/aiManager.ts +326 -262
- package/src/managers/backgroundTaskManager.ts +1 -1
- package/src/managers/bangManager.ts +1 -1
- package/src/managers/hookManager.ts +6 -6
- package/src/managers/mcpManager.ts +2 -4
- package/src/managers/messageManager.ts +1 -9
- package/src/managers/toolManager.ts +18 -12
- package/src/services/aiService.ts +23 -12
- package/src/services/authService.ts +1 -31
- package/src/services/configurationService.ts +21 -0
- package/src/services/fileWatcher.ts +15 -1
- package/src/services/jsonlHandler.ts +7 -27
- package/src/services/session.ts +16 -57
- package/src/tools/bashTool.ts +3 -1
- package/src/tools/editTool.ts +31 -1
- package/src/tools/enterPlanMode.ts +9 -0
- package/src/tools/exitPlanMode.ts +10 -0
- package/src/tools/readTool.ts +20 -10
- package/src/tools/types.ts +15 -3
- package/src/tools/writeTool.ts +15 -1
- package/src/types/agent.ts +0 -2
- package/src/types/config.ts +1 -0
- package/src/utils/cacheControlUtils.ts +77 -95
- package/src/utils/constants.ts +1 -1
- package/src/utils/containerSetup.ts +3 -7
- 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. | `
|
|
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
|
-
*
|
|
73
|
-
*
|
|
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
|
|
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":"
|
|
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
|
|
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
|
-
*
|
|
126
|
-
*
|
|
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
|
-
|
|
130
|
-
const messages = [];
|
|
130
|
+
maybeAddPlanModeMessage(currentMode) {
|
|
131
131
|
if (!this.permissionManager)
|
|
132
|
-
return
|
|
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
|
-
|
|
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
|
|
144
|
+
return;
|
|
145
145
|
const planFilePath = this.permissionManager.getPlanFilePath();
|
|
146
146
|
if (!planFilePath)
|
|
147
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
-
|
|
786
|
-
|
|
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
|
-
|
|
867
|
-
|
|
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:
|
|
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
|
|
@@ -718,8 +718,7 @@ export class HookManager {
|
|
|
718
718
|
transcriptPath,
|
|
719
719
|
cwd: this.workdir,
|
|
720
720
|
endSource: source,
|
|
721
|
-
env:
|
|
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:
|
|
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) {
|