wave-agent-sdk 0.0.1
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/README.md +32 -0
- package/dist/agent.d.ts +96 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +286 -0
- package/dist/hooks/executor.d.ts +56 -0
- package/dist/hooks/executor.d.ts.map +1 -0
- package/dist/hooks/executor.js +312 -0
- package/dist/hooks/index.d.ts +17 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +14 -0
- package/dist/hooks/manager.d.ts +90 -0
- package/dist/hooks/manager.d.ts.map +1 -0
- package/dist/hooks/manager.js +395 -0
- package/dist/hooks/matcher.d.ts +49 -0
- package/dist/hooks/matcher.d.ts.map +1 -0
- package/dist/hooks/matcher.js +147 -0
- package/dist/hooks/settings.d.ts +46 -0
- package/dist/hooks/settings.d.ts.map +1 -0
- package/dist/hooks/settings.js +100 -0
- package/dist/hooks/types.d.ts +80 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +59 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/managers/aiManager.d.ts +61 -0
- package/dist/managers/aiManager.d.ts.map +1 -0
- package/dist/managers/aiManager.js +415 -0
- package/dist/managers/backgroundBashManager.d.ts +27 -0
- package/dist/managers/backgroundBashManager.d.ts.map +1 -0
- package/dist/managers/backgroundBashManager.js +166 -0
- package/dist/managers/bashManager.d.ts +20 -0
- package/dist/managers/bashManager.d.ts.map +1 -0
- package/dist/managers/bashManager.js +66 -0
- package/dist/managers/mcpManager.d.ts +63 -0
- package/dist/managers/mcpManager.d.ts.map +1 -0
- package/dist/managers/mcpManager.js +378 -0
- package/dist/managers/messageManager.d.ts +85 -0
- package/dist/managers/messageManager.d.ts.map +1 -0
- package/dist/managers/messageManager.js +265 -0
- package/dist/managers/skillManager.d.ts +59 -0
- package/dist/managers/skillManager.d.ts.map +1 -0
- package/dist/managers/skillManager.js +317 -0
- package/dist/managers/slashCommandManager.d.ts +77 -0
- package/dist/managers/slashCommandManager.d.ts.map +1 -0
- package/dist/managers/slashCommandManager.js +208 -0
- package/dist/managers/toolManager.d.ts +23 -0
- package/dist/managers/toolManager.d.ts.map +1 -0
- package/dist/managers/toolManager.js +79 -0
- package/dist/services/aiService.d.ts +28 -0
- package/dist/services/aiService.d.ts.map +1 -0
- package/dist/services/aiService.js +180 -0
- package/dist/services/memory.d.ts +8 -0
- package/dist/services/memory.d.ts.map +1 -0
- package/dist/services/memory.js +128 -0
- package/dist/services/session.d.ts +54 -0
- package/dist/services/session.d.ts.map +1 -0
- package/dist/services/session.js +196 -0
- package/dist/tools/bashTool.d.ts +14 -0
- package/dist/tools/bashTool.d.ts.map +1 -0
- package/dist/tools/bashTool.js +351 -0
- package/dist/tools/deleteFileTool.d.ts +6 -0
- package/dist/tools/deleteFileTool.d.ts.map +1 -0
- package/dist/tools/deleteFileTool.js +67 -0
- package/dist/tools/editTool.d.ts +6 -0
- package/dist/tools/editTool.d.ts.map +1 -0
- package/dist/tools/editTool.js +168 -0
- package/dist/tools/globTool.d.ts +6 -0
- package/dist/tools/globTool.d.ts.map +1 -0
- package/dist/tools/globTool.js +113 -0
- package/dist/tools/grepTool.d.ts +6 -0
- package/dist/tools/grepTool.d.ts.map +1 -0
- package/dist/tools/grepTool.js +268 -0
- package/dist/tools/lsTool.d.ts +6 -0
- package/dist/tools/lsTool.d.ts.map +1 -0
- package/dist/tools/lsTool.js +160 -0
- package/dist/tools/multiEditTool.d.ts +6 -0
- package/dist/tools/multiEditTool.d.ts.map +1 -0
- package/dist/tools/multiEditTool.js +222 -0
- package/dist/tools/readTool.d.ts +6 -0
- package/dist/tools/readTool.d.ts.map +1 -0
- package/dist/tools/readTool.js +136 -0
- package/dist/tools/types.d.ts +35 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/writeTool.d.ts +6 -0
- package/dist/tools/writeTool.d.ts.map +1 -0
- package/dist/tools/writeTool.js +138 -0
- package/dist/types.d.ts +212 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/utils/bashHistory.d.ts +46 -0
- package/dist/utils/bashHistory.d.ts.map +1 -0
- package/dist/utils/bashHistory.js +236 -0
- package/dist/utils/commandArgumentParser.d.ts +34 -0
- package/dist/utils/commandArgumentParser.d.ts.map +1 -0
- package/dist/utils/commandArgumentParser.js +123 -0
- package/dist/utils/constants.d.ts +27 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +28 -0
- package/dist/utils/convertMessagesForAPI.d.ts +9 -0
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -0
- package/dist/utils/convertMessagesForAPI.js +189 -0
- package/dist/utils/customCommands.d.ts +14 -0
- package/dist/utils/customCommands.d.ts.map +1 -0
- package/dist/utils/customCommands.js +71 -0
- package/dist/utils/fileFilter.d.ts +26 -0
- package/dist/utils/fileFilter.d.ts.map +1 -0
- package/dist/utils/fileFilter.js +177 -0
- package/dist/utils/markdownParser.d.ts +27 -0
- package/dist/utils/markdownParser.d.ts.map +1 -0
- package/dist/utils/markdownParser.js +109 -0
- package/dist/utils/mcpUtils.d.ts +24 -0
- package/dist/utils/mcpUtils.d.ts.map +1 -0
- package/dist/utils/mcpUtils.js +51 -0
- package/dist/utils/messageOperations.d.ts +118 -0
- package/dist/utils/messageOperations.d.ts.map +1 -0
- package/dist/utils/messageOperations.js +334 -0
- package/dist/utils/path.d.ts +25 -0
- package/dist/utils/path.d.ts.map +1 -0
- package/dist/utils/path.js +109 -0
- package/dist/utils/skillParser.d.ts +18 -0
- package/dist/utils/skillParser.d.ts.map +1 -0
- package/dist/utils/skillParser.js +147 -0
- package/dist/utils/stringUtils.d.ts +13 -0
- package/dist/utils/stringUtils.d.ts.map +1 -0
- package/dist/utils/stringUtils.js +44 -0
- package/package.json +51 -0
- package/src/agent.ts +405 -0
- package/src/hooks/executor.ts +440 -0
- package/src/hooks/index.ts +52 -0
- package/src/hooks/manager.ts +618 -0
- package/src/hooks/matcher.ts +187 -0
- package/src/hooks/settings.ts +129 -0
- package/src/hooks/types.ts +169 -0
- package/src/index.ts +24 -0
- package/src/managers/aiManager.ts +573 -0
- package/src/managers/backgroundBashManager.ts +203 -0
- package/src/managers/bashManager.ts +97 -0
- package/src/managers/mcpManager.ts +493 -0
- package/src/managers/messageManager.ts +415 -0
- package/src/managers/skillManager.ts +404 -0
- package/src/managers/slashCommandManager.ts +293 -0
- package/src/managers/toolManager.ts +106 -0
- package/src/services/aiService.ts +252 -0
- package/src/services/memory.ts +149 -0
- package/src/services/session.ts +265 -0
- package/src/tools/bashTool.ts +402 -0
- package/src/tools/deleteFileTool.ts +81 -0
- package/src/tools/editTool.ts +192 -0
- package/src/tools/globTool.ts +135 -0
- package/src/tools/grepTool.ts +326 -0
- package/src/tools/lsTool.ts +187 -0
- package/src/tools/multiEditTool.ts +268 -0
- package/src/tools/readTool.ts +165 -0
- package/src/tools/types.ts +47 -0
- package/src/tools/writeTool.ts +163 -0
- package/src/types.ts +260 -0
- package/src/utils/bashHistory.ts +303 -0
- package/src/utils/commandArgumentParser.ts +153 -0
- package/src/utils/constants.ts +37 -0
- package/src/utils/convertMessagesForAPI.ts +236 -0
- package/src/utils/customCommands.ts +85 -0
- package/src/utils/fileFilter.ts +202 -0
- package/src/utils/markdownParser.ts +156 -0
- package/src/utils/mcpUtils.ts +81 -0
- package/src/utils/messageOperations.ts +506 -0
- package/src/utils/path.ts +118 -0
- package/src/utils/skillParser.ts +188 -0
- package/src/utils/stringUtils.ts +50 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { callAgent, compressMessages } from "../services/aiService.js";
|
|
2
|
+
import { getMessagesToCompress } from "../utils/messageOperations.js";
|
|
3
|
+
import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
|
|
4
|
+
import * as memory from "../services/memory.js";
|
|
5
|
+
import { DEFAULT_TOKEN_LIMIT } from "../utils/constants.js";
|
|
6
|
+
export class AIManager {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.isLoading = false;
|
|
9
|
+
this.abortController = null;
|
|
10
|
+
this.toolAbortController = null;
|
|
11
|
+
this.isCompressing = false;
|
|
12
|
+
this.messageManager = options.messageManager;
|
|
13
|
+
this.toolManager = options.toolManager;
|
|
14
|
+
this.backgroundBashManager = options.backgroundBashManager;
|
|
15
|
+
this.hookManager = options.hookManager;
|
|
16
|
+
this.logger = options.logger;
|
|
17
|
+
this.workdir = options.workdir;
|
|
18
|
+
this.systemPrompt = options.systemPrompt;
|
|
19
|
+
this.callbacks = options.callbacks ?? {};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get filtered tool configuration
|
|
23
|
+
*/
|
|
24
|
+
getFilteredToolsConfig(allowedTools) {
|
|
25
|
+
const allTools = this.toolManager.getToolsConfig();
|
|
26
|
+
// If no allowedTools specified, return all tools
|
|
27
|
+
if (!allowedTools || allowedTools.length === 0) {
|
|
28
|
+
return allTools;
|
|
29
|
+
}
|
|
30
|
+
// Filter allowed tools
|
|
31
|
+
return allTools.filter((tool) => allowedTools.includes(tool.function.name));
|
|
32
|
+
}
|
|
33
|
+
setIsLoading(isLoading) {
|
|
34
|
+
this.isLoading = isLoading;
|
|
35
|
+
}
|
|
36
|
+
abortAIMessage() {
|
|
37
|
+
// Interrupt AI service
|
|
38
|
+
if (this.abortController) {
|
|
39
|
+
try {
|
|
40
|
+
this.abortController.abort();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
this.logger?.error("Failed to abort AI service:", error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Interrupt tool execution
|
|
47
|
+
if (this.toolAbortController) {
|
|
48
|
+
try {
|
|
49
|
+
this.toolAbortController.abort();
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
this.logger?.error("Failed to abort tool execution:", error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.setIsLoading(false);
|
|
56
|
+
}
|
|
57
|
+
// Helper method to generate compactParams
|
|
58
|
+
generateCompactParams(toolName, toolArgs) {
|
|
59
|
+
try {
|
|
60
|
+
const toolPlugin = this.toolManager
|
|
61
|
+
.list()
|
|
62
|
+
.find((plugin) => plugin.name === toolName);
|
|
63
|
+
if (toolPlugin?.formatCompactParams) {
|
|
64
|
+
const context = {
|
|
65
|
+
workdir: this.workdir,
|
|
66
|
+
};
|
|
67
|
+
return toolPlugin.formatCompactParams(toolArgs, context);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
this.logger?.warn("Failed to generate compactParams", error);
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
// Private method to handle token statistics and message compression
|
|
76
|
+
async handleTokenUsageAndCompression(usage, abortController) {
|
|
77
|
+
if (!usage)
|
|
78
|
+
return;
|
|
79
|
+
// Update token statistics - display latest token usage
|
|
80
|
+
this.messageManager.setlatestTotalTokens(usage.total_tokens);
|
|
81
|
+
// Check if token limit exceeded
|
|
82
|
+
const tokenLimit = parseInt(process.env.TOKEN_LIMIT || `${DEFAULT_TOKEN_LIMIT}`, 10);
|
|
83
|
+
if (usage.total_tokens > tokenLimit) {
|
|
84
|
+
this.logger?.info(`Token usage exceeded ${tokenLimit}, compressing messages...`);
|
|
85
|
+
// Check if messages need compression
|
|
86
|
+
const { messagesToCompress, insertIndex } = getMessagesToCompress(this.messageManager.getMessages(), 7);
|
|
87
|
+
// If there are messages to compress, perform compression
|
|
88
|
+
if (messagesToCompress.length > 0) {
|
|
89
|
+
const recentChatMessages = convertMessagesForAPI(messagesToCompress);
|
|
90
|
+
this.setIsCompressing(true);
|
|
91
|
+
try {
|
|
92
|
+
const compressedContent = await compressMessages({
|
|
93
|
+
messages: recentChatMessages,
|
|
94
|
+
abortSignal: abortController.signal,
|
|
95
|
+
});
|
|
96
|
+
// Execute message reconstruction and sessionId update after compression
|
|
97
|
+
this.messageManager.compressMessagesAndUpdateSession(insertIndex, compressedContent);
|
|
98
|
+
this.logger?.info(`Successfully compressed ${messagesToCompress.length} messages and updated session`);
|
|
99
|
+
}
|
|
100
|
+
catch (compressError) {
|
|
101
|
+
this.logger?.error("Failed to compress messages:", compressError);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
this.setIsCompressing(false);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
getIsCompressing() {
|
|
110
|
+
return this.isCompressing;
|
|
111
|
+
}
|
|
112
|
+
setIsCompressing(isCompressing) {
|
|
113
|
+
if (this.isCompressing !== isCompressing) {
|
|
114
|
+
this.isCompressing = isCompressing;
|
|
115
|
+
this.callbacks.onCompressionStateChange?.(isCompressing);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async sendAIMessage(options = {}) {
|
|
119
|
+
const { recursionDepth = 0, model, allowedTools } = options;
|
|
120
|
+
// Only check isLoading for the initial call (recursionDepth === 0)
|
|
121
|
+
if (recursionDepth === 0 && this.isLoading) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Create new AbortController
|
|
125
|
+
const abortController = new AbortController();
|
|
126
|
+
this.abortController = abortController;
|
|
127
|
+
// Create separate AbortController for tool execution
|
|
128
|
+
const toolAbortController = new AbortController();
|
|
129
|
+
this.toolAbortController = toolAbortController;
|
|
130
|
+
// Only set loading state for the initial call
|
|
131
|
+
if (recursionDepth === 0) {
|
|
132
|
+
this.setIsLoading(true);
|
|
133
|
+
}
|
|
134
|
+
// Get recent message history
|
|
135
|
+
const recentMessages = convertMessagesForAPI(this.messageManager.getMessages());
|
|
136
|
+
try {
|
|
137
|
+
// Get combined memory content
|
|
138
|
+
const combinedMemory = await memory.getCombinedMemoryContent(this.workdir);
|
|
139
|
+
// Call AI service (non-streaming)
|
|
140
|
+
const result = await callAgent({
|
|
141
|
+
messages: recentMessages,
|
|
142
|
+
sessionId: this.messageManager.getSessionId(),
|
|
143
|
+
abortSignal: abortController.signal,
|
|
144
|
+
memory: combinedMemory, // Pass combined memory content
|
|
145
|
+
workdir: this.workdir, // Pass working directory
|
|
146
|
+
tools: this.getFilteredToolsConfig(allowedTools), // Pass filtered tool configuration
|
|
147
|
+
model: model, // Use passed model
|
|
148
|
+
systemPrompt: this.systemPrompt, // Pass custom system prompt
|
|
149
|
+
});
|
|
150
|
+
// Collect content and tool calls
|
|
151
|
+
const content = result.content || "";
|
|
152
|
+
const toolCalls = [];
|
|
153
|
+
if (result.tool_calls) {
|
|
154
|
+
for (const toolCall of result.tool_calls) {
|
|
155
|
+
if (toolCall.type === "function") {
|
|
156
|
+
toolCalls.push(toolCall);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Add assistant message at once (including content and tool calls)
|
|
161
|
+
this.messageManager.addAssistantMessage(content, toolCalls);
|
|
162
|
+
if (toolCalls.length > 0) {
|
|
163
|
+
for (const functionToolCall of toolCalls) {
|
|
164
|
+
const toolId = functionToolCall.id || "";
|
|
165
|
+
// Execute tool
|
|
166
|
+
try {
|
|
167
|
+
// Check if already interrupted, skip tool execution if so
|
|
168
|
+
if (abortController.signal.aborted ||
|
|
169
|
+
toolAbortController.signal.aborted) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Safely parse tool parameters, handle tools without parameters
|
|
173
|
+
let toolArgs = {};
|
|
174
|
+
const argsString = functionToolCall.function?.arguments?.trim();
|
|
175
|
+
if (!argsString || argsString === "") {
|
|
176
|
+
// Tool without parameters, use empty object
|
|
177
|
+
toolArgs = {};
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
try {
|
|
181
|
+
toolArgs = JSON.parse(argsString);
|
|
182
|
+
}
|
|
183
|
+
catch (parseError) {
|
|
184
|
+
// For non-empty but malformed JSON, still throw exception
|
|
185
|
+
const errorMessage = `Failed to parse tool arguments: ${argsString}`;
|
|
186
|
+
this.logger?.error(errorMessage, parseError);
|
|
187
|
+
throw new Error(errorMessage);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Set tool start execution state
|
|
191
|
+
const toolName = functionToolCall.function?.name || "";
|
|
192
|
+
const compactParams = this.generateCompactParams(toolName, toolArgs);
|
|
193
|
+
this.messageManager.updateToolBlock({
|
|
194
|
+
toolId,
|
|
195
|
+
args: JSON.stringify(toolArgs, null, 2),
|
|
196
|
+
isRunning: true, // isRunning: true
|
|
197
|
+
name: toolName,
|
|
198
|
+
compactParams,
|
|
199
|
+
});
|
|
200
|
+
try {
|
|
201
|
+
// Execute PreToolUse hooks before tool execution
|
|
202
|
+
await this.executePreToolUseHooks(toolName, toolArgs);
|
|
203
|
+
// Create tool execution context
|
|
204
|
+
const context = {
|
|
205
|
+
abortSignal: toolAbortController.signal,
|
|
206
|
+
backgroundBashManager: this.backgroundBashManager,
|
|
207
|
+
workdir: this.workdir,
|
|
208
|
+
};
|
|
209
|
+
// Execute tool
|
|
210
|
+
const toolResult = await this.toolManager.execute(functionToolCall.function?.name || "", toolArgs, context);
|
|
211
|
+
// Update message state - tool execution completed
|
|
212
|
+
this.messageManager.updateToolBlock({
|
|
213
|
+
toolId,
|
|
214
|
+
args: JSON.stringify(toolArgs, null, 2),
|
|
215
|
+
result: toolResult.content ||
|
|
216
|
+
(toolResult.error ? `Error: ${toolResult.error}` : ""),
|
|
217
|
+
success: toolResult.success,
|
|
218
|
+
error: toolResult.error,
|
|
219
|
+
isRunning: false, // isRunning: false
|
|
220
|
+
name: toolName,
|
|
221
|
+
shortResult: toolResult.shortResult,
|
|
222
|
+
compactParams,
|
|
223
|
+
});
|
|
224
|
+
// If tool returns diff information, add diff block
|
|
225
|
+
if (toolResult.success &&
|
|
226
|
+
toolResult.diffResult &&
|
|
227
|
+
toolResult.filePath) {
|
|
228
|
+
this.messageManager.addDiffBlock(toolResult.filePath, toolResult.diffResult);
|
|
229
|
+
}
|
|
230
|
+
// Execute PostToolUse hooks after successful tool completion
|
|
231
|
+
await this.executePostToolUseHooks(toolName, toolArgs, toolResult);
|
|
232
|
+
}
|
|
233
|
+
catch (toolError) {
|
|
234
|
+
const errorMessage = toolError instanceof Error
|
|
235
|
+
? toolError.message
|
|
236
|
+
: String(toolError);
|
|
237
|
+
this.messageManager.updateToolBlock({
|
|
238
|
+
toolId,
|
|
239
|
+
args: JSON.stringify(toolArgs, null, 2),
|
|
240
|
+
result: `Tool execution failed: ${errorMessage}`,
|
|
241
|
+
success: false,
|
|
242
|
+
error: errorMessage,
|
|
243
|
+
isRunning: false,
|
|
244
|
+
name: toolName,
|
|
245
|
+
compactParams,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (parseError) {
|
|
250
|
+
// Check if it's a parsing error due to interruption
|
|
251
|
+
const isAborted = abortController.signal.aborted ||
|
|
252
|
+
toolAbortController.signal.aborted;
|
|
253
|
+
if (isAborted) {
|
|
254
|
+
// If interrupted, return directly without showing error
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const errorMessage = parseError instanceof Error
|
|
258
|
+
? parseError.message
|
|
259
|
+
: String(parseError);
|
|
260
|
+
this.messageManager.addErrorBlock(`Failed to parse tool arguments for ${functionToolCall.function?.name}: ${errorMessage}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Handle token statistics and message compression
|
|
265
|
+
await this.handleTokenUsageAndCompression(result.usage, abortController);
|
|
266
|
+
// Check if there are tool operations, if so automatically initiate next AI service call
|
|
267
|
+
if (toolCalls.length > 0) {
|
|
268
|
+
// Check interruption status
|
|
269
|
+
const isCurrentlyAborted = abortController.signal.aborted || toolAbortController.signal.aborted;
|
|
270
|
+
// AI service call ends, clear abort controller
|
|
271
|
+
this.abortController = null;
|
|
272
|
+
// Clear tool AbortController after tool execution completes
|
|
273
|
+
this.toolAbortController = null;
|
|
274
|
+
if (!isCurrentlyAborted) {
|
|
275
|
+
// Recursively call AI service, increment recursion depth, and pass same configuration
|
|
276
|
+
await this.sendAIMessage({
|
|
277
|
+
recursionDepth: recursionDepth + 1,
|
|
278
|
+
model,
|
|
279
|
+
allowedTools,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
// Clear abort controller when no tool operations
|
|
285
|
+
this.abortController = null;
|
|
286
|
+
this.toolAbortController = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
// Check if error is due to user interrupt operation
|
|
291
|
+
const isAborted = abortController.signal.aborted ||
|
|
292
|
+
toolAbortController.signal.aborted ||
|
|
293
|
+
(error instanceof Error &&
|
|
294
|
+
(error.name === "AbortError" || error.message.includes("aborted")));
|
|
295
|
+
if (!isAborted) {
|
|
296
|
+
this.messageManager.addErrorBlock(error instanceof Error ? error.message : "Unknown error occurred");
|
|
297
|
+
}
|
|
298
|
+
// Reset abort controller on error
|
|
299
|
+
this.abortController = null;
|
|
300
|
+
this.toolAbortController = null;
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
// Only clear loading state for the initial call
|
|
304
|
+
if (recursionDepth === 0) {
|
|
305
|
+
this.setIsLoading(false);
|
|
306
|
+
// Save session before executing Stop hooks
|
|
307
|
+
await this.messageManager.saveSession();
|
|
308
|
+
// Execute Stop hooks when AI response cycle completes
|
|
309
|
+
await this.executeStopHooks();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Execute Stop hooks when AI response cycle completes
|
|
315
|
+
*/
|
|
316
|
+
async executeStopHooks() {
|
|
317
|
+
if (!this.hookManager)
|
|
318
|
+
return;
|
|
319
|
+
try {
|
|
320
|
+
const context = {
|
|
321
|
+
event: "Stop",
|
|
322
|
+
projectDir: this.workdir,
|
|
323
|
+
timestamp: new Date(),
|
|
324
|
+
sessionId: this.messageManager.getSessionId(),
|
|
325
|
+
transcriptPath: this.messageManager.getTranscriptPath(),
|
|
326
|
+
cwd: this.workdir,
|
|
327
|
+
// Stop hooks don't need toolName, toolInput, toolResponse, or userPrompt
|
|
328
|
+
};
|
|
329
|
+
const results = await this.hookManager.executeHooks("Stop", context);
|
|
330
|
+
// Log hook execution results for debugging
|
|
331
|
+
if (results.length > 0) {
|
|
332
|
+
this.logger?.debug(`Executed ${results.length} Stop hook(s):`, results.map((r) => ({
|
|
333
|
+
success: r.success,
|
|
334
|
+
duration: r.duration,
|
|
335
|
+
exitCode: r.exitCode,
|
|
336
|
+
timedOut: r.timedOut,
|
|
337
|
+
stderr: r.stderr,
|
|
338
|
+
})));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
// Hook execution errors should not interrupt the main workflow
|
|
343
|
+
this.logger?.error("Stop hook execution failed:", error);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Execute PreToolUse hooks before tool execution
|
|
348
|
+
*/
|
|
349
|
+
async executePreToolUseHooks(toolName, toolInput) {
|
|
350
|
+
if (!this.hookManager)
|
|
351
|
+
return;
|
|
352
|
+
try {
|
|
353
|
+
const context = {
|
|
354
|
+
event: "PreToolUse",
|
|
355
|
+
projectDir: this.workdir,
|
|
356
|
+
timestamp: new Date(),
|
|
357
|
+
toolName,
|
|
358
|
+
sessionId: this.messageManager.getSessionId(),
|
|
359
|
+
transcriptPath: this.messageManager.getTranscriptPath(),
|
|
360
|
+
cwd: this.workdir,
|
|
361
|
+
toolInput,
|
|
362
|
+
};
|
|
363
|
+
const results = await this.hookManager.executeHooks("PreToolUse", context);
|
|
364
|
+
// Log hook execution results for debugging
|
|
365
|
+
if (results.length > 0) {
|
|
366
|
+
this.logger?.debug(`Executed ${results.length} PreToolUse hook(s) for ${toolName}:`, results.map((r) => ({
|
|
367
|
+
success: r.success,
|
|
368
|
+
duration: r.duration,
|
|
369
|
+
exitCode: r.exitCode,
|
|
370
|
+
timedOut: r.timedOut,
|
|
371
|
+
stderr: r.stderr,
|
|
372
|
+
})));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
// Hook execution errors should not interrupt the main workflow
|
|
377
|
+
this.logger?.error("PreToolUse hook execution failed:", error);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Execute PostToolUse hooks after tool completion
|
|
382
|
+
*/
|
|
383
|
+
async executePostToolUseHooks(toolName, toolInput, toolResponse) {
|
|
384
|
+
if (!this.hookManager)
|
|
385
|
+
return;
|
|
386
|
+
try {
|
|
387
|
+
const context = {
|
|
388
|
+
event: "PostToolUse",
|
|
389
|
+
projectDir: this.workdir,
|
|
390
|
+
timestamp: new Date(),
|
|
391
|
+
toolName,
|
|
392
|
+
sessionId: this.messageManager.getSessionId(),
|
|
393
|
+
transcriptPath: this.messageManager.getTranscriptPath(),
|
|
394
|
+
cwd: this.workdir,
|
|
395
|
+
toolInput,
|
|
396
|
+
toolResponse,
|
|
397
|
+
};
|
|
398
|
+
const results = await this.hookManager.executeHooks("PostToolUse", context);
|
|
399
|
+
// Log hook execution results for debugging
|
|
400
|
+
if (results.length > 0) {
|
|
401
|
+
this.logger?.debug(`Executed ${results.length} PostToolUse hook(s) for ${toolName}:`, results.map((r) => ({
|
|
402
|
+
success: r.success,
|
|
403
|
+
duration: r.duration,
|
|
404
|
+
exitCode: r.exitCode,
|
|
405
|
+
timedOut: r.timedOut,
|
|
406
|
+
stderr: r.stderr,
|
|
407
|
+
})));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
// Hook execution errors should not interrupt the main workflow
|
|
412
|
+
this.logger?.error("PostToolUse hook execution failed:", error);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BackgroundShell } from "../types.js";
|
|
2
|
+
export interface BackgroundBashManagerCallbacks {
|
|
3
|
+
onShellsChange?: (shells: BackgroundShell[]) => void;
|
|
4
|
+
}
|
|
5
|
+
export interface BackgroundBashManagerOptions {
|
|
6
|
+
callbacks?: BackgroundBashManagerCallbacks;
|
|
7
|
+
workdir: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class BackgroundBashManager {
|
|
10
|
+
private shells;
|
|
11
|
+
private nextId;
|
|
12
|
+
private callbacks;
|
|
13
|
+
private workdir;
|
|
14
|
+
constructor(options: BackgroundBashManagerOptions);
|
|
15
|
+
private notifyShellsChange;
|
|
16
|
+
startShell(command: string, timeout?: number): string;
|
|
17
|
+
getShell(id: string): BackgroundShell | undefined;
|
|
18
|
+
getAllShells(): BackgroundShell[];
|
|
19
|
+
getOutput(id: string, filter?: string): {
|
|
20
|
+
stdout: string;
|
|
21
|
+
stderr: string;
|
|
22
|
+
status: string;
|
|
23
|
+
} | null;
|
|
24
|
+
killShell(id: string): boolean;
|
|
25
|
+
cleanup(): void;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=backgroundBashManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backgroundBashManager.d.ts","sourceRoot":"","sources":["../../src/managers/backgroundBashManager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,MAAM,WAAW,8BAA8B;IAC7C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,EAAE,8BAA8B,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAsC;IACpD,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,4BAA4B;IAKjD,OAAO,CAAC,kBAAkB;IAInB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM;IAsErD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIjD,YAAY,IAAI,eAAe,EAAE;IAIjC,SAAS,CACd,EAAE,EAAE,MAAM,EACV,MAAM,CAAC,EAAE,MAAM,GACd;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAiCrD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAmD9B,OAAO,IAAI,IAAI;CAUvB"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
export class BackgroundBashManager {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
this.shells = new Map();
|
|
5
|
+
this.nextId = 1;
|
|
6
|
+
this.callbacks = options.callbacks || {};
|
|
7
|
+
this.workdir = options.workdir;
|
|
8
|
+
}
|
|
9
|
+
notifyShellsChange() {
|
|
10
|
+
this.callbacks.onShellsChange?.(Array.from(this.shells.values()));
|
|
11
|
+
}
|
|
12
|
+
startShell(command, timeout) {
|
|
13
|
+
const id = `bash_${this.nextId++}`;
|
|
14
|
+
const startTime = Date.now();
|
|
15
|
+
const child = spawn(command, {
|
|
16
|
+
shell: true,
|
|
17
|
+
stdio: "pipe",
|
|
18
|
+
cwd: this.workdir,
|
|
19
|
+
env: {
|
|
20
|
+
...process.env,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
const shell = {
|
|
24
|
+
id,
|
|
25
|
+
process: child,
|
|
26
|
+
command,
|
|
27
|
+
startTime,
|
|
28
|
+
status: "running",
|
|
29
|
+
stdout: "",
|
|
30
|
+
stderr: "",
|
|
31
|
+
};
|
|
32
|
+
this.shells.set(id, shell);
|
|
33
|
+
this.notifyShellsChange();
|
|
34
|
+
// Set up timeout if specified
|
|
35
|
+
let timeoutHandle;
|
|
36
|
+
if (timeout && timeout > 0) {
|
|
37
|
+
timeoutHandle = setTimeout(() => {
|
|
38
|
+
if (shell.status === "running") {
|
|
39
|
+
this.killShell(id);
|
|
40
|
+
}
|
|
41
|
+
}, timeout);
|
|
42
|
+
}
|
|
43
|
+
child.stdout?.on("data", (data) => {
|
|
44
|
+
shell.stdout += data.toString();
|
|
45
|
+
this.notifyShellsChange();
|
|
46
|
+
});
|
|
47
|
+
child.stderr?.on("data", (data) => {
|
|
48
|
+
shell.stderr += data.toString();
|
|
49
|
+
this.notifyShellsChange();
|
|
50
|
+
});
|
|
51
|
+
child.on("exit", (code) => {
|
|
52
|
+
if (timeoutHandle) {
|
|
53
|
+
clearTimeout(timeoutHandle);
|
|
54
|
+
}
|
|
55
|
+
shell.status = "completed";
|
|
56
|
+
shell.exitCode = code ?? 0;
|
|
57
|
+
shell.runtime = Date.now() - startTime;
|
|
58
|
+
this.notifyShellsChange();
|
|
59
|
+
});
|
|
60
|
+
child.on("error", (error) => {
|
|
61
|
+
if (timeoutHandle) {
|
|
62
|
+
clearTimeout(timeoutHandle);
|
|
63
|
+
}
|
|
64
|
+
shell.status = "completed";
|
|
65
|
+
shell.stderr += `\nProcess error: ${error.message}`;
|
|
66
|
+
shell.exitCode = 1;
|
|
67
|
+
shell.runtime = Date.now() - startTime;
|
|
68
|
+
this.notifyShellsChange();
|
|
69
|
+
});
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
getShell(id) {
|
|
73
|
+
return this.shells.get(id);
|
|
74
|
+
}
|
|
75
|
+
getAllShells() {
|
|
76
|
+
return Array.from(this.shells.values());
|
|
77
|
+
}
|
|
78
|
+
getOutput(id, filter) {
|
|
79
|
+
const shell = this.shells.get(id);
|
|
80
|
+
if (!shell) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
let stdout = shell.stdout;
|
|
84
|
+
let stderr = shell.stderr;
|
|
85
|
+
// Apply regex filter if provided
|
|
86
|
+
if (filter) {
|
|
87
|
+
try {
|
|
88
|
+
const regex = new RegExp(filter);
|
|
89
|
+
stdout = stdout
|
|
90
|
+
.split("\n")
|
|
91
|
+
.filter((line) => regex.test(line))
|
|
92
|
+
.join("\n");
|
|
93
|
+
stderr = stderr
|
|
94
|
+
.split("\n")
|
|
95
|
+
.filter((line) => regex.test(line))
|
|
96
|
+
.join("\n");
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// logger.warn(`Invalid filter regex: ${filter}`, error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
stdout,
|
|
104
|
+
stderr,
|
|
105
|
+
status: shell.status,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
killShell(id) {
|
|
109
|
+
const shell = this.shells.get(id);
|
|
110
|
+
if (!shell || shell.status !== "running") {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
// Try to kill process group first
|
|
115
|
+
if (shell.process.pid) {
|
|
116
|
+
process.kill(-shell.process.pid, "SIGTERM");
|
|
117
|
+
// Force kill after timeout
|
|
118
|
+
setTimeout(() => {
|
|
119
|
+
if (shell.status === "running" &&
|
|
120
|
+
shell.process.pid &&
|
|
121
|
+
!shell.process.killed) {
|
|
122
|
+
try {
|
|
123
|
+
process.kill(-shell.process.pid, "SIGKILL");
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// logger.error("Failed to force kill process:", error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}, 1000);
|
|
130
|
+
}
|
|
131
|
+
shell.status = "killed";
|
|
132
|
+
shell.runtime = Date.now() - shell.startTime;
|
|
133
|
+
this.notifyShellsChange();
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Fallback to direct process kill
|
|
138
|
+
try {
|
|
139
|
+
shell.process.kill("SIGTERM");
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
if (!shell.process.killed) {
|
|
142
|
+
shell.process.kill("SIGKILL");
|
|
143
|
+
}
|
|
144
|
+
}, 1000);
|
|
145
|
+
shell.status = "killed";
|
|
146
|
+
shell.runtime = Date.now() - shell.startTime;
|
|
147
|
+
this.notifyShellsChange();
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// logger.error("Failed to kill child process:", directKillError);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
cleanup() {
|
|
157
|
+
// Kill all running shells
|
|
158
|
+
for (const [id, shell] of this.shells) {
|
|
159
|
+
if (shell.status === "running") {
|
|
160
|
+
this.killShell(id);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
this.shells.clear();
|
|
164
|
+
this.notifyShellsChange();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { MessageManager } from "./messageManager.js";
|
|
2
|
+
export interface BashManagerOptions {
|
|
3
|
+
messageManager: MessageManager;
|
|
4
|
+
workdir: string;
|
|
5
|
+
}
|
|
6
|
+
export interface CommandExecutionResult {
|
|
7
|
+
exitCode: number;
|
|
8
|
+
output: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class BashManager {
|
|
11
|
+
private workdir;
|
|
12
|
+
private messageManager;
|
|
13
|
+
isCommandRunning: boolean;
|
|
14
|
+
private currentProcess;
|
|
15
|
+
constructor(options: BashManagerOptions);
|
|
16
|
+
private setCommandRunning;
|
|
17
|
+
executeCommand(command: string): Promise<number>;
|
|
18
|
+
abortCommand(): void;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=bashManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bashManager.d.ts","sourceRoot":"","sources":["../../src/managers/bashManager.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,cAAc,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAiB;IAChC,gBAAgB,UAAS;IAChC,OAAO,CAAC,cAAc,CAA6B;gBAEvC,OAAO,EAAE,kBAAkB;IAKvC,OAAO,CAAC,iBAAiB;IAIZ,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA4DtD,YAAY,IAAI,IAAI;CAO5B"}
|