wave-agent-sdk 0.17.9 → 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.
- package/builtin/skills/settings/ENV.md +1 -1
- package/dist/managers/aiManager.d.ts +10 -4
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +214 -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/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +13 -0
- 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/package.json +1 -1
- package/src/managers/aiManager.ts +292 -263
- 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/configurationService.ts +21 -0
- 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
|
@@ -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
|
-
*
|
|
242
|
-
*
|
|
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
|
|
248
|
+
private maybeAddPlanModeMessage(
|
|
246
249
|
currentMode: PermissionMode | undefined,
|
|
247
|
-
):
|
|
248
|
-
|
|
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
|
-
|
|
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
|
|
264
|
+
if (currentMode !== "plan") return;
|
|
264
265
|
|
|
265
266
|
const planFilePath = this.permissionManager.getPlanFilePath();
|
|
266
|
-
if (!planFilePath) return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
-
|
|
1687
|
-
|
|
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(
|