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
|
@@ -4,7 +4,11 @@ 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
|
|
7
|
+
import {
|
|
8
|
+
getTaskReminderTurnCounts,
|
|
9
|
+
maybeInjectTaskReminder,
|
|
10
|
+
TASK_REMINDER_CONFIG,
|
|
11
|
+
} from "../utils/taskReminder.js";
|
|
8
12
|
import { existsSync } from "node:fs";
|
|
9
13
|
import type {
|
|
10
14
|
GatewayConfig,
|
|
@@ -18,6 +22,7 @@ import type { ToolContext, ToolResult } from "../tools/types.js";
|
|
|
18
22
|
import type { MessageManager } from "./messageManager.js";
|
|
19
23
|
import type { BackgroundTaskManager } from "./backgroundTaskManager.js";
|
|
20
24
|
import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
|
|
25
|
+
import type { ChatCompletionMessageParam } from "openai/resources.js";
|
|
21
26
|
import type { HookManager } from "./hookManager.js";
|
|
22
27
|
import type { ExtendedHookExecutionContext } from "../types/hooks.js";
|
|
23
28
|
import type { PermissionManager } from "./permissionManager.js";
|
|
@@ -75,6 +80,11 @@ export class AIManager {
|
|
|
75
80
|
private originalWorkdir: string;
|
|
76
81
|
private consecutiveCompactionFailures: number = 0;
|
|
77
82
|
private readonly maxTurns?: number;
|
|
83
|
+
/** Tracks file mtime/hash at read time for staleness detection on Edit/Write */
|
|
84
|
+
private readFileState = new Map<
|
|
85
|
+
string,
|
|
86
|
+
{ mtime: number; hash: string; offset?: number; limit?: number }
|
|
87
|
+
>();
|
|
78
88
|
/** Override tool_choice for this AI manager (e.g. for structured output) */
|
|
79
89
|
public toolChoiceOverride?:
|
|
80
90
|
| "auto"
|
|
@@ -238,32 +248,29 @@ export class AIManager {
|
|
|
238
248
|
}
|
|
239
249
|
|
|
240
250
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* This preserves prompt caching by keeping the system prompt constant.
|
|
251
|
+
* Add plan mode reminder as a persistent meta message to the message manager.
|
|
252
|
+
* Called before getMessages() so the stored message is included in the API call.
|
|
244
253
|
*/
|
|
245
|
-
private
|
|
254
|
+
private maybeAddPlanModeMessage(
|
|
246
255
|
currentMode: PermissionMode | undefined,
|
|
247
|
-
):
|
|
248
|
-
|
|
249
|
-
[];
|
|
250
|
-
if (!this.permissionManager) return messages;
|
|
256
|
+
): void {
|
|
257
|
+
if (!this.permissionManager) return;
|
|
251
258
|
|
|
252
259
|
// Handle exit notification (one-time after leaving plan mode)
|
|
253
260
|
if (this.permissionManager.getNeedsPlanModeExitAttachment()) {
|
|
254
261
|
const planFilePath = this.permissionManager.getPlanFilePath();
|
|
255
262
|
const planExists = planFilePath ? existsSync(planFilePath) : false;
|
|
256
|
-
|
|
257
|
-
role: "user",
|
|
263
|
+
this.messageManager.addUserMessage({
|
|
258
264
|
content: buildExitedPlanModeReminder(planFilePath, planExists),
|
|
265
|
+
isMeta: true,
|
|
259
266
|
});
|
|
260
267
|
this.permissionManager.setNeedsPlanModeExitAttachment(false);
|
|
261
268
|
}
|
|
262
269
|
|
|
263
|
-
if (currentMode !== "plan") return
|
|
270
|
+
if (currentMode !== "plan") return;
|
|
264
271
|
|
|
265
272
|
const planFilePath = this.permissionManager.getPlanFilePath();
|
|
266
|
-
if (!planFilePath) return
|
|
273
|
+
if (!planFilePath) return;
|
|
267
274
|
|
|
268
275
|
const planExists = existsSync(planFilePath);
|
|
269
276
|
|
|
@@ -272,25 +279,46 @@ export class AIManager {
|
|
|
272
279
|
if (planMgr?.isPlanEntryReminderPending()) {
|
|
273
280
|
if (this.permissionManager.hasExitedPlanModeInSession() && planExists) {
|
|
274
281
|
// Re-entry: use small reminder
|
|
275
|
-
|
|
276
|
-
role: "user",
|
|
282
|
+
this.messageManager.addUserMessage({
|
|
277
283
|
content: buildPlanModeReEntryReminder(planFilePath),
|
|
284
|
+
isMeta: true,
|
|
278
285
|
});
|
|
279
286
|
} else {
|
|
280
287
|
// First entry: use full reminder
|
|
281
|
-
|
|
282
|
-
role: "user",
|
|
288
|
+
this.messageManager.addUserMessage({
|
|
283
289
|
content: buildPlanModeReminder(
|
|
284
290
|
planFilePath,
|
|
285
291
|
planExists,
|
|
286
292
|
!!this.subagentType,
|
|
287
293
|
),
|
|
294
|
+
isMeta: true,
|
|
288
295
|
});
|
|
289
296
|
}
|
|
290
297
|
planMgr.consumePlanEntryReminder();
|
|
291
298
|
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private async maybeInjectTaskReminderIntoMessages(
|
|
302
|
+
messages: ChatCompletionMessageParam[],
|
|
303
|
+
toolNames: Set<string>,
|
|
304
|
+
): Promise<void> {
|
|
305
|
+
// Guard: no task tools available
|
|
306
|
+
if (!toolNames.has("TaskUpdate")) return;
|
|
307
|
+
|
|
308
|
+
const internalMessages = this.messageManager.getMessages();
|
|
309
|
+
const turnCounts = getTaskReminderTurnCounts(internalMessages);
|
|
310
|
+
|
|
311
|
+
if (
|
|
312
|
+
turnCounts.turnsSinceLastTaskManagement <
|
|
313
|
+
TASK_REMINDER_CONFIG.TURNS_SINCE_WRITE ||
|
|
314
|
+
turnCounts.turnsSinceLastReminder <
|
|
315
|
+
TASK_REMINDER_CONFIG.TURNS_BETWEEN_REMINDERS
|
|
316
|
+
) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
292
319
|
|
|
293
|
-
|
|
320
|
+
const tasks = await this.taskManager.listTasks();
|
|
321
|
+
maybeInjectTaskReminder(messages, turnCounts, tasks);
|
|
294
322
|
}
|
|
295
323
|
|
|
296
324
|
public setIsLoading(isLoading: boolean): void {
|
|
@@ -470,6 +498,25 @@ export class AIManager {
|
|
|
470
498
|
compactUsage,
|
|
471
499
|
);
|
|
472
500
|
|
|
501
|
+
// Re-add plan mode reminder as persistent meta message after compaction
|
|
502
|
+
const postCompactMode = this.permissionManager?.getCurrentEffectiveMode(
|
|
503
|
+
this.getModelConfig().permissionMode,
|
|
504
|
+
);
|
|
505
|
+
if (postCompactMode === "plan") {
|
|
506
|
+
const planFilePath = this.permissionManager?.getPlanFilePath();
|
|
507
|
+
if (planFilePath) {
|
|
508
|
+
const planExists = existsSync(planFilePath);
|
|
509
|
+
this.messageManager.addUserMessage({
|
|
510
|
+
content: buildPlanModeReminder(
|
|
511
|
+
planFilePath,
|
|
512
|
+
planExists,
|
|
513
|
+
!!this.subagentType,
|
|
514
|
+
),
|
|
515
|
+
isMeta: true,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
473
520
|
// 8. Track usage
|
|
474
521
|
if (compactUsage && this.callbacks?.onUsageAdded) {
|
|
475
522
|
this.callbacks.onUsageAdded(compactUsage);
|
|
@@ -569,26 +616,6 @@ export class AIManager {
|
|
|
569
616
|
if (usedTokens >= POST_COMPACT_TOKEN_BUDGET) break;
|
|
570
617
|
}
|
|
571
618
|
|
|
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
619
|
// 4. Invoked skills context (with token budget, matching Claude Code)
|
|
593
620
|
const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000;
|
|
594
621
|
const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000;
|
|
@@ -784,6 +811,14 @@ export class AIManager {
|
|
|
784
811
|
toolAbortController = this.toolAbortController!;
|
|
785
812
|
}
|
|
786
813
|
|
|
814
|
+
// Get current permission mode
|
|
815
|
+
const currentMode = this.permissionManager?.getCurrentEffectiveMode(
|
|
816
|
+
this.getModelConfig().permissionMode,
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
// Add plan mode reminder as persistent meta message before getting messages
|
|
820
|
+
this.maybeAddPlanModeMessage(currentMode);
|
|
821
|
+
|
|
787
822
|
// Get recent message history with microcompact applied
|
|
788
823
|
const rawMessages = this.messageManager.getMessages();
|
|
789
824
|
const microcompactedMessages = microcompactMessages(rawMessages, {
|
|
@@ -801,23 +836,12 @@ export class AIManager {
|
|
|
801
836
|
|
|
802
837
|
logger?.debug("modelConfig in sendAIMessage", this.getModelConfig());
|
|
803
838
|
|
|
804
|
-
// Get current permission mode and plan file path
|
|
805
|
-
const currentMode = this.permissionManager?.getCurrentEffectiveMode(
|
|
806
|
-
this.getModelConfig().permissionMode,
|
|
807
|
-
);
|
|
808
839
|
const toolsConfig = this.getFilteredToolsConfig();
|
|
809
840
|
const toolNames = new Set(toolsConfig.map((t) => t.function.name));
|
|
810
841
|
const filteredToolPlugins = this.toolManager
|
|
811
842
|
.getTools()
|
|
812
843
|
.filter((t) => toolNames.has(t.name));
|
|
813
844
|
|
|
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
845
|
let autoMemoryOptions: { directory: string; content: string } | undefined;
|
|
822
846
|
|
|
823
847
|
if (this.getAutoMemoryEnabled()) {
|
|
@@ -857,6 +881,12 @@ export class AIManager {
|
|
|
857
881
|
toolChoice: this.toolChoiceOverride, // Pass tool_choice override
|
|
858
882
|
};
|
|
859
883
|
|
|
884
|
+
// Inject task reminder if needed (not persisted, regenerated each turn)
|
|
885
|
+
await this.maybeInjectTaskReminderIntoMessages(
|
|
886
|
+
callAgentOptions.messages,
|
|
887
|
+
toolNames,
|
|
888
|
+
);
|
|
889
|
+
|
|
860
890
|
// Add streaming callbacks only if streaming is enabled
|
|
861
891
|
if (this.stream) {
|
|
862
892
|
callAgentOptions.onContentUpdate = (content: string) => {
|
|
@@ -1001,218 +1031,52 @@ export class AIManager {
|
|
|
1001
1031
|
}
|
|
1002
1032
|
|
|
1003
1033
|
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
|
-
}
|
|
1034
|
+
// Partition tool calls into batches: consecutive concurrency-safe
|
|
1035
|
+
// tools run in parallel, non-safe tools (Edit, Write, MCP) run one
|
|
1036
|
+
// at a time to prevent read-modify-write races on the same file.
|
|
1037
|
+
const batches: {
|
|
1038
|
+
calls: ChatCompletionMessageFunctionToolCall[];
|
|
1039
|
+
safe: boolean;
|
|
1040
|
+
}[] = [];
|
|
1041
|
+
for (const call of toolCalls) {
|
|
1042
|
+
const toolName = call.function?.name || "";
|
|
1043
|
+
const safe = this.toolManager.isConcurrencySafe(toolName);
|
|
1044
|
+
const lastBatch = batches[batches.length - 1];
|
|
1045
|
+
if (lastBatch && lastBatch.safe && safe) {
|
|
1046
|
+
lastBatch.calls.push(call);
|
|
1047
|
+
} else {
|
|
1048
|
+
batches.push({ calls: [call], safe });
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1061
1051
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1052
|
+
// Execute batches sequentially; within a safe batch, tools run in parallel
|
|
1053
|
+
for (const batch of batches) {
|
|
1054
|
+
if (
|
|
1055
|
+
abortController.signal.aborted ||
|
|
1056
|
+
toolAbortController.signal.aborted
|
|
1057
|
+
) {
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1060
|
+
if (batch.calls.length === 1) {
|
|
1061
|
+
await this.executeToolCall(
|
|
1062
|
+
batch.calls[0],
|
|
1063
|
+
abortController,
|
|
1064
|
+
toolAbortController,
|
|
1065
|
+
result.finish_reason,
|
|
1065
1066
|
);
|
|
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);
|
|
1067
|
+
} else {
|
|
1068
|
+
await Promise.all(
|
|
1069
|
+
batch.calls.map((call) =>
|
|
1070
|
+
this.executeToolCall(
|
|
1071
|
+
call,
|
|
1072
|
+
abortController,
|
|
1073
|
+
toolAbortController,
|
|
1074
|
+
result.finish_reason,
|
|
1075
|
+
),
|
|
1076
|
+
),
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1216
1080
|
}
|
|
1217
1081
|
|
|
1218
1082
|
// Handle token statistics and message compaction
|
|
@@ -1584,6 +1448,206 @@ export class AIManager {
|
|
|
1584
1448
|
}
|
|
1585
1449
|
}
|
|
1586
1450
|
|
|
1451
|
+
/**
|
|
1452
|
+
* Execute a single tool call: arg parsing, block emission, PreToolUse hooks,
|
|
1453
|
+
* tool execution, PostToolUse hooks, and error handling.
|
|
1454
|
+
*/
|
|
1455
|
+
private async executeToolCall(
|
|
1456
|
+
functionToolCall: ChatCompletionMessageFunctionToolCall,
|
|
1457
|
+
abortController: AbortController,
|
|
1458
|
+
toolAbortController: AbortController,
|
|
1459
|
+
finishReason?: string | null,
|
|
1460
|
+
): Promise<void> {
|
|
1461
|
+
const toolId = functionToolCall.id || "";
|
|
1462
|
+
|
|
1463
|
+
// Check if already interrupted, skip tool execution if so
|
|
1464
|
+
if (abortController.signal.aborted || toolAbortController.signal.aborted) {
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const toolName = functionToolCall.function?.name || "";
|
|
1469
|
+
// Safely parse tool parameters, handle tools without parameters
|
|
1470
|
+
let toolArgs: Record<string, unknown> = {};
|
|
1471
|
+
let jsonRecovered = false;
|
|
1472
|
+
const argsString = functionToolCall.function?.arguments?.trim();
|
|
1473
|
+
|
|
1474
|
+
if (!argsString || argsString === "") {
|
|
1475
|
+
// Tool without parameters, use empty object
|
|
1476
|
+
toolArgs = {};
|
|
1477
|
+
} else {
|
|
1478
|
+
let recoveredArgs = argsString;
|
|
1479
|
+
try {
|
|
1480
|
+
toolArgs = JSON.parse(argsString);
|
|
1481
|
+
} catch {
|
|
1482
|
+
// Attempt to recover truncated JSON (e.g., missing closing braces)
|
|
1483
|
+
recoveredArgs = recoverTruncatedJson(argsString);
|
|
1484
|
+
try {
|
|
1485
|
+
toolArgs = JSON.parse(recoveredArgs);
|
|
1486
|
+
jsonRecovered = true;
|
|
1487
|
+
logger.warn(`Recovered truncated JSON for tool "${toolName}"`);
|
|
1488
|
+
} catch (parseError) {
|
|
1489
|
+
let errorMessage = `Failed to parse tool arguments`;
|
|
1490
|
+
if (finishReason === "length") {
|
|
1491
|
+
errorMessage += " (output truncated, please reduce your output)";
|
|
1492
|
+
}
|
|
1493
|
+
logger?.error(errorMessage, parseError);
|
|
1494
|
+
this.messageManager.updateToolBlock({
|
|
1495
|
+
id: toolId,
|
|
1496
|
+
parameters: argsString,
|
|
1497
|
+
result: errorMessage,
|
|
1498
|
+
success: false,
|
|
1499
|
+
error: errorMessage,
|
|
1500
|
+
stage: "end",
|
|
1501
|
+
name: toolName,
|
|
1502
|
+
compactParams: "",
|
|
1503
|
+
timestamp: Date.now(),
|
|
1504
|
+
});
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const compactParams = this.generateCompactParams(toolName, toolArgs);
|
|
1511
|
+
|
|
1512
|
+
// Emit start stage for non-streaming tool calls
|
|
1513
|
+
if (!this.stream) {
|
|
1514
|
+
this.messageManager.updateToolBlock({
|
|
1515
|
+
id: toolId,
|
|
1516
|
+
stage: "start",
|
|
1517
|
+
name: toolName,
|
|
1518
|
+
compactParams,
|
|
1519
|
+
parameters: argsString,
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Emit running stage (tool execution about to start)
|
|
1524
|
+
this.messageManager.updateToolBlock({
|
|
1525
|
+
id: toolId,
|
|
1526
|
+
stage: "running",
|
|
1527
|
+
name: toolName,
|
|
1528
|
+
compactParams,
|
|
1529
|
+
parameters: argsString,
|
|
1530
|
+
parametersChunk: "",
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
try {
|
|
1534
|
+
// Execute PreToolUse hooks before tool execution
|
|
1535
|
+
const shouldExecuteTool = await this.executePreToolUseHooks(
|
|
1536
|
+
toolName,
|
|
1537
|
+
toolArgs,
|
|
1538
|
+
toolId,
|
|
1539
|
+
);
|
|
1540
|
+
|
|
1541
|
+
// If PreToolUse hooks blocked execution, skip tool execution
|
|
1542
|
+
if (!shouldExecuteTool) {
|
|
1543
|
+
logger?.info(`Tool ${toolName} execution blocked by PreToolUse hooks`);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Create tool execution context
|
|
1548
|
+
const context: ToolContext = {
|
|
1549
|
+
abortSignal: toolAbortController.signal,
|
|
1550
|
+
backgroundTaskManager: this.backgroundTaskManager,
|
|
1551
|
+
workdir: this.getWorkdir(),
|
|
1552
|
+
originalWorkdir: this.originalWorkdir,
|
|
1553
|
+
messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
|
|
1554
|
+
sessionId: this.messageManager.getSessionId(),
|
|
1555
|
+
toolCallId: toolId,
|
|
1556
|
+
taskManager: this.taskManager,
|
|
1557
|
+
readFileState: this.readFileState,
|
|
1558
|
+
onShortResultUpdate: (shortResult: string) => {
|
|
1559
|
+
this.messageManager.updateToolBlock({
|
|
1560
|
+
id: toolId,
|
|
1561
|
+
shortResult,
|
|
1562
|
+
stage: "running",
|
|
1563
|
+
});
|
|
1564
|
+
},
|
|
1565
|
+
onResultUpdate: (result: string) => {
|
|
1566
|
+
this.messageManager.updateToolBlock({
|
|
1567
|
+
id: toolId,
|
|
1568
|
+
result,
|
|
1569
|
+
stage: "running",
|
|
1570
|
+
});
|
|
1571
|
+
},
|
|
1572
|
+
onCwdChange: async (newCwd: string) => {
|
|
1573
|
+
const oldCwd = this.getWorkdir();
|
|
1574
|
+
this.container.register("Workdir", newCwd);
|
|
1575
|
+
this._onCwdChange?.(newCwd);
|
|
1576
|
+
if (this.hookManager) {
|
|
1577
|
+
const sessionId = this.messageManager.getSessionId();
|
|
1578
|
+
const transcriptPath = this.messageManager.getTranscriptPath();
|
|
1579
|
+
const env = Object.fromEntries(
|
|
1580
|
+
Object.entries(process.env).filter((e) => e[1] !== undefined),
|
|
1581
|
+
) as Record<string, string>;
|
|
1582
|
+
await this.hookManager.executeCwdChangedHooks(
|
|
1583
|
+
oldCwd,
|
|
1584
|
+
newCwd,
|
|
1585
|
+
sessionId,
|
|
1586
|
+
transcriptPath,
|
|
1587
|
+
env,
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
},
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
// Execute tool
|
|
1594
|
+
const toolResult = await this.toolManager.execute(
|
|
1595
|
+
functionToolCall.function?.name || "",
|
|
1596
|
+
toolArgs,
|
|
1597
|
+
context,
|
|
1598
|
+
);
|
|
1599
|
+
|
|
1600
|
+
// Build result content, adding truncation warning if JSON was recovered
|
|
1601
|
+
let toolResultContent =
|
|
1602
|
+
toolResult.content ||
|
|
1603
|
+
(toolResult.error ? `Error: ${toolResult.error}` : "");
|
|
1604
|
+
if (jsonRecovered) {
|
|
1605
|
+
toolResultContent +=
|
|
1606
|
+
"\n\nTool arguments were truncated (likely exceeded max output tokens). Please reduce your output or split into multiple tool calls.";
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Update message state - tool execution completed
|
|
1610
|
+
this.messageManager.updateToolBlock({
|
|
1611
|
+
id: toolId,
|
|
1612
|
+
parameters: argsString,
|
|
1613
|
+
result: toolResultContent,
|
|
1614
|
+
success: toolResult.success,
|
|
1615
|
+
error: toolResult.error,
|
|
1616
|
+
stage: "end",
|
|
1617
|
+
name: toolName,
|
|
1618
|
+
compactParams,
|
|
1619
|
+
shortResult: toolResult.shortResult,
|
|
1620
|
+
isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
|
|
1621
|
+
startLineNumber: toolResult.startLineNumber,
|
|
1622
|
+
timestamp: Date.now(),
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
// Execute PostToolUse hooks after successful tool completion
|
|
1626
|
+
await this.executePostToolUseHooks(
|
|
1627
|
+
toolId,
|
|
1628
|
+
toolName,
|
|
1629
|
+
toolArgs,
|
|
1630
|
+
toolResult,
|
|
1631
|
+
);
|
|
1632
|
+
} catch (toolError) {
|
|
1633
|
+
const errorMessage =
|
|
1634
|
+
toolError instanceof Error ? toolError.message : String(toolError);
|
|
1635
|
+
|
|
1636
|
+
this.messageManager.updateToolBlock({
|
|
1637
|
+
id: toolId,
|
|
1638
|
+
parameters: JSON.stringify(toolArgs, null, 2),
|
|
1639
|
+
result: `Tool execution failed: ${errorMessage}`,
|
|
1640
|
+
success: false,
|
|
1641
|
+
error: errorMessage,
|
|
1642
|
+
stage: "end",
|
|
1643
|
+
name: toolName,
|
|
1644
|
+
compactParams,
|
|
1645
|
+
isManuallyBackgrounded: false,
|
|
1646
|
+
timestamp: Date.now(),
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1587
1651
|
/**
|
|
1588
1652
|
* Execute PreToolUse hooks before tool execution
|
|
1589
1653
|
* Returns true if hooks allow tool execution, false if blocked
|
|
@@ -1682,9 +1746,9 @@ export class AIManager {
|
|
|
1682
1746
|
toolResponse,
|
|
1683
1747
|
subagentType: this.subagentType, // Include subagent type in hook context
|
|
1684
1748
|
planFilePath: this.permissionManager?.getPlanFilePath(),
|
|
1685
|
-
env:
|
|
1686
|
-
|
|
1687
|
-
|
|
1749
|
+
env: Object.fromEntries(
|
|
1750
|
+
Object.entries(process.env).filter((e) => e[1] !== undefined),
|
|
1751
|
+
) as Record<string, string>, // Include environment variables
|
|
1688
1752
|
};
|
|
1689
1753
|
|
|
1690
1754
|
const results = await this.hookManager.executeHooks(
|