wave-agent-sdk 0.2.1 → 0.5.0
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/dist/agent.d.ts +66 -20
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +156 -83
- package/dist/constants/prompts.d.ts +7 -2
- package/dist/constants/prompts.d.ts.map +1 -1
- package/dist/constants/prompts.js +41 -5
- package/dist/constants/tools.d.ts +2 -2
- package/dist/constants/tools.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
- package/dist/managers/MemoryRuleManager.js +16 -2
- package/dist/managers/aiManager.d.ts +14 -4
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +61 -9
- package/dist/managers/backgroundBashManager.d.ts.map +1 -1
- package/dist/managers/backgroundBashManager.js +1 -0
- package/dist/managers/backgroundTaskManager.d.ts +35 -0
- package/dist/managers/backgroundTaskManager.d.ts.map +1 -0
- package/dist/managers/backgroundTaskManager.js +249 -0
- package/dist/managers/bashManager.d.ts.map +1 -1
- package/dist/managers/bashManager.js +0 -3
- package/dist/managers/foregroundTaskManager.d.ts +9 -0
- package/dist/managers/foregroundTaskManager.d.ts.map +1 -0
- package/dist/managers/foregroundTaskManager.js +20 -0
- package/dist/managers/liveConfigManager.d.ts +1 -1
- package/dist/managers/liveConfigManager.d.ts.map +1 -1
- package/dist/managers/lspManager.d.ts.map +1 -1
- package/dist/managers/lspManager.js +3 -1
- package/dist/managers/messageManager.d.ts +34 -4
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +104 -13
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +11 -13
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +3 -2
- package/dist/managers/pluginScopeManager.d.ts +13 -2
- package/dist/managers/pluginScopeManager.d.ts.map +1 -1
- package/dist/managers/pluginScopeManager.js +38 -0
- package/dist/managers/reversionManager.d.ts +39 -0
- package/dist/managers/reversionManager.d.ts.map +1 -0
- package/dist/managers/reversionManager.js +118 -0
- package/dist/managers/slashCommandManager.d.ts +4 -1
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +16 -6
- package/dist/managers/subagentManager.d.ts +13 -2
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +144 -35
- package/dist/managers/toolManager.d.ts +11 -1
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +11 -3
- package/dist/services/GitService.d.ts.map +1 -1
- package/dist/services/GitService.js +6 -2
- package/dist/services/MarketplaceService.d.ts +14 -1
- package/dist/services/MarketplaceService.d.ts.map +1 -1
- package/dist/services/MarketplaceService.js +72 -4
- package/dist/services/MemoryRuleService.d.ts +1 -1
- package/dist/services/MemoryRuleService.d.ts.map +1 -1
- package/dist/services/MemoryRuleService.js +13 -2
- package/dist/services/aiService.js +1 -1
- package/dist/services/configurationService.d.ts +18 -2
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +62 -0
- package/dist/services/fileWatcher.d.ts +0 -5
- package/dist/services/fileWatcher.d.ts.map +1 -1
- package/dist/services/fileWatcher.js +0 -11
- package/dist/services/memory.js +1 -1
- package/dist/services/pluginLoader.d.ts.map +1 -1
- package/dist/services/pluginLoader.js +6 -1
- package/dist/services/reversionService.d.ts +24 -0
- package/dist/services/reversionService.d.ts.map +1 -0
- package/dist/services/reversionService.js +76 -0
- package/dist/services/session.d.ts +7 -0
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +126 -3
- package/dist/tools/bashTool.d.ts +0 -8
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +52 -174
- package/dist/tools/deleteFileTool.d.ts.map +1 -1
- package/dist/tools/deleteFileTool.js +9 -0
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +15 -4
- package/dist/tools/multiEditTool.d.ts.map +1 -1
- package/dist/tools/multiEditTool.js +16 -5
- package/dist/tools/taskOutputTool.d.ts +3 -0
- package/dist/tools/taskOutputTool.d.ts.map +1 -0
- package/dist/tools/taskOutputTool.js +149 -0
- package/dist/tools/taskStopTool.d.ts +3 -0
- package/dist/tools/taskStopTool.d.ts.map +1 -0
- package/dist/tools/taskStopTool.js +65 -0
- package/dist/tools/taskTool.d.ts.map +1 -1
- package/dist/tools/taskTool.js +105 -63
- package/dist/tools/types.d.ts +7 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +9 -0
- package/dist/types/commands.d.ts +1 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/configuration.d.ts +3 -0
- package/dist/types/configuration.d.ts.map +1 -1
- package/dist/types/environment.d.ts +2 -1
- package/dist/types/environment.d.ts.map +1 -1
- package/dist/types/environment.js +0 -6
- package/dist/types/history.d.ts +5 -0
- package/dist/types/history.d.ts.map +1 -0
- package/dist/types/history.js +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/marketplace.d.ts +4 -0
- package/dist/types/marketplace.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +7 -1
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/processes.d.ts +24 -4
- package/dist/types/processes.d.ts.map +1 -1
- package/dist/types/reversion.d.ts +29 -0
- package/dist/types/reversion.d.ts.map +1 -0
- package/dist/types/reversion.js +1 -0
- package/dist/utils/builtinSubagents.d.ts.map +1 -1
- package/dist/utils/builtinSubagents.js +16 -0
- package/dist/utils/constants.d.ts +2 -2
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +2 -2
- package/dist/utils/editUtils.d.ts +4 -9
- package/dist/utils/editUtils.d.ts.map +1 -1
- package/dist/utils/editUtils.js +54 -55
- package/dist/utils/messageOperations.d.ts +3 -1
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +8 -1
- package/dist/utils/openaiClient.d.ts.map +1 -1
- package/dist/utils/openaiClient.js +56 -26
- package/dist/utils/promptHistory.d.ts +20 -0
- package/dist/utils/promptHistory.d.ts.map +1 -0
- package/dist/utils/promptHistory.js +117 -0
- package/package.json +5 -3
- package/src/agent.ts +193 -109
- package/src/constants/prompts.ts +45 -5
- package/src/constants/tools.ts +2 -2
- package/src/index.ts +1 -1
- package/src/managers/MemoryRuleManager.ts +18 -2
- package/src/managers/aiManager.ts +87 -18
- package/src/managers/backgroundBashManager.ts +1 -0
- package/src/managers/backgroundTaskManager.ts +306 -0
- package/src/managers/bashManager.ts +0 -4
- package/src/managers/foregroundTaskManager.ts +26 -0
- package/src/managers/liveConfigManager.ts +2 -1
- package/src/managers/lspManager.ts +3 -1
- package/src/managers/messageManager.ts +136 -18
- package/src/managers/permissionManager.ts +11 -13
- package/src/managers/pluginManager.ts +4 -3
- package/src/managers/pluginScopeManager.ts +57 -8
- package/src/managers/reversionManager.ts +152 -0
- package/src/managers/slashCommandManager.ts +30 -7
- package/src/managers/subagentManager.ts +176 -31
- package/src/managers/toolManager.ts +23 -4
- package/src/services/GitService.ts +6 -2
- package/src/services/MarketplaceService.ts +100 -4
- package/src/services/MemoryRuleService.ts +18 -6
- package/src/services/aiService.ts +1 -1
- package/src/services/configurationService.ts +79 -1
- package/src/services/fileWatcher.ts +0 -13
- package/src/services/memory.ts +1 -1
- package/src/services/pluginLoader.ts +7 -1
- package/src/services/reversionService.ts +94 -0
- package/src/services/session.ts +161 -3
- package/src/tools/bashTool.ts +73 -200
- package/src/tools/deleteFileTool.ts +15 -0
- package/src/tools/editTool.ts +20 -10
- package/src/tools/multiEditTool.ts +21 -11
- package/src/tools/taskOutputTool.ts +174 -0
- package/src/tools/taskStopTool.ts +72 -0
- package/src/tools/taskTool.ts +130 -74
- package/src/tools/types.ts +7 -0
- package/src/tools/writeTool.ts +14 -0
- package/src/types/commands.ts +3 -0
- package/src/types/configuration.ts +4 -0
- package/src/types/environment.ts +3 -1
- package/src/types/history.ts +4 -0
- package/src/types/index.ts +1 -0
- package/src/types/marketplace.ts +5 -0
- package/src/types/messaging.ts +9 -1
- package/src/types/processes.ts +33 -4
- package/src/types/reversion.ts +29 -0
- package/src/utils/builtinSubagents.ts +18 -0
- package/src/utils/constants.ts +2 -2
- package/src/utils/editUtils.ts +66 -58
- package/src/utils/messageOperations.ts +10 -0
- package/src/utils/openaiClient.ts +69 -35
- package/src/utils/promptHistory.ts +133 -0
- package/dist/utils/bashHistory.d.ts +0 -50
- package/dist/utils/bashHistory.d.ts.map +0 -1
- package/dist/utils/bashHistory.js +0 -256
- package/src/utils/bashHistory.ts +0 -320
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
import { getMessagesToCompress } from "../utils/messageOperations.js";
|
|
7
7
|
import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
|
|
8
8
|
import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
|
|
9
|
-
import * as memory from "../services/memory.js";
|
|
10
9
|
import * as fs from "node:fs/promises";
|
|
11
10
|
import type {
|
|
12
11
|
Logger,
|
|
@@ -17,7 +16,7 @@ import type {
|
|
|
17
16
|
import type { ToolManager } from "./toolManager.js";
|
|
18
17
|
import type { ToolContext, ToolResult } from "../tools/types.js";
|
|
19
18
|
import type { MessageManager } from "./messageManager.js";
|
|
20
|
-
import type {
|
|
19
|
+
import type { BackgroundTaskManager } from "./backgroundTaskManager.js";
|
|
21
20
|
import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
|
|
22
21
|
import type { HookManager } from "./hookManager.js";
|
|
23
22
|
import type { ExtendedHookExecutionContext } from "../types/hooks.js";
|
|
@@ -36,19 +35,21 @@ export interface AIManagerOptions {
|
|
|
36
35
|
messageManager: MessageManager;
|
|
37
36
|
toolManager: ToolManager;
|
|
38
37
|
logger?: Logger;
|
|
39
|
-
|
|
38
|
+
backgroundTaskManager?: BackgroundTaskManager;
|
|
40
39
|
hookManager?: HookManager;
|
|
41
40
|
permissionManager?: PermissionManager;
|
|
42
41
|
callbacks?: AIManagerCallbacks;
|
|
43
42
|
workdir: string;
|
|
44
43
|
systemPrompt?: string;
|
|
45
44
|
subagentType?: string; // Optional subagent type for hook context
|
|
45
|
+
reversionManager?: import("./reversionManager.js").ReversionManager;
|
|
46
46
|
/**Whether to use streaming mode for AI responses - defaults to true */
|
|
47
47
|
stream?: boolean;
|
|
48
48
|
// Dynamic configuration getters
|
|
49
49
|
getGatewayConfig: () => GatewayConfig;
|
|
50
50
|
getModelConfig: () => ModelConfig;
|
|
51
51
|
getMaxInputTokens: () => number;
|
|
52
|
+
getLanguage: () => string | undefined;
|
|
52
53
|
getEnvironmentVars?: () => Record<string, string>; // Get configuration environment variables for hooks
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -59,8 +60,9 @@ export class AIManager {
|
|
|
59
60
|
private logger?: Logger;
|
|
60
61
|
private toolManager: ToolManager;
|
|
61
62
|
private messageManager: MessageManager;
|
|
62
|
-
private
|
|
63
|
+
private backgroundTaskManager?: BackgroundTaskManager;
|
|
63
64
|
private hookManager?: HookManager;
|
|
65
|
+
private reversionManager?: import("./reversionManager.js").ReversionManager;
|
|
64
66
|
private permissionManager?: PermissionManager;
|
|
65
67
|
private workdir: string;
|
|
66
68
|
private systemPrompt?: string;
|
|
@@ -71,13 +73,15 @@ export class AIManager {
|
|
|
71
73
|
private getGatewayConfigFn: () => GatewayConfig;
|
|
72
74
|
private getModelConfigFn: () => ModelConfig;
|
|
73
75
|
private getMaxInputTokensFn: () => number;
|
|
76
|
+
private getLanguageFn: () => string | undefined;
|
|
74
77
|
private getEnvironmentVarsFn?: () => Record<string, string>;
|
|
75
78
|
|
|
76
79
|
constructor(options: AIManagerOptions) {
|
|
77
80
|
this.messageManager = options.messageManager;
|
|
78
81
|
this.toolManager = options.toolManager;
|
|
79
|
-
this.
|
|
82
|
+
this.backgroundTaskManager = options.backgroundTaskManager;
|
|
80
83
|
this.hookManager = options.hookManager;
|
|
84
|
+
this.reversionManager = options.reversionManager;
|
|
81
85
|
this.permissionManager = options.permissionManager;
|
|
82
86
|
this.logger = options.logger;
|
|
83
87
|
this.workdir = options.workdir;
|
|
@@ -90,6 +94,7 @@ export class AIManager {
|
|
|
90
94
|
this.getGatewayConfigFn = options.getGatewayConfig;
|
|
91
95
|
this.getModelConfigFn = options.getModelConfig;
|
|
92
96
|
this.getMaxInputTokensFn = options.getMaxInputTokens;
|
|
97
|
+
this.getLanguageFn = options.getLanguage;
|
|
93
98
|
this.getEnvironmentVarsFn = options.getEnvironmentVars;
|
|
94
99
|
}
|
|
95
100
|
|
|
@@ -106,6 +111,10 @@ export class AIManager {
|
|
|
106
111
|
return this.getMaxInputTokensFn();
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
public getLanguage(): string | undefined {
|
|
115
|
+
return this.getLanguageFn();
|
|
116
|
+
}
|
|
117
|
+
|
|
109
118
|
private isCompressing: boolean = false;
|
|
110
119
|
private callbacks: AIManagerCallbacks;
|
|
111
120
|
|
|
@@ -150,6 +159,15 @@ export class AIManager {
|
|
|
150
159
|
this.setIsLoading(false);
|
|
151
160
|
}
|
|
152
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Abort the AI recursion loop immediately.
|
|
164
|
+
* This is used when a tool is backgrounded via Ctrl-B, even if no foreground task was active.
|
|
165
|
+
*/
|
|
166
|
+
public abortRecursion(): void {
|
|
167
|
+
this.logger?.info("Aborting AI recursion loop");
|
|
168
|
+
this.abortAIMessage();
|
|
169
|
+
}
|
|
170
|
+
|
|
153
171
|
// Helper method to generate compactParams
|
|
154
172
|
private generateCompactParams(
|
|
155
173
|
toolName: string,
|
|
@@ -267,7 +285,7 @@ export class AIManager {
|
|
|
267
285
|
options: {
|
|
268
286
|
recursionDepth?: number;
|
|
269
287
|
model?: string;
|
|
270
|
-
/** Rules for automatic tool approval (e.g., "Bash(git status
|
|
288
|
+
/** Rules for automatic tool approval (e.g., "Bash(git status*)") */
|
|
271
289
|
allowedRules?: string[];
|
|
272
290
|
/** List of tools available to the AI (e.g., ["Bash", "Read"]) */
|
|
273
291
|
tools?: string[];
|
|
@@ -287,9 +305,6 @@ export class AIManager {
|
|
|
287
305
|
return;
|
|
288
306
|
}
|
|
289
307
|
|
|
290
|
-
// Save session in each recursion to ensure message persistence
|
|
291
|
-
await this.messageManager.saveSession();
|
|
292
|
-
|
|
293
308
|
// Only create new AbortControllers for the initial call (recursionDepth === 0)
|
|
294
309
|
// For recursive calls, reuse existing controllers to maintain abort signal
|
|
295
310
|
let abortController: AbortController;
|
|
@@ -304,10 +319,14 @@ export class AIManager {
|
|
|
304
319
|
this.toolAbortController = toolAbortController;
|
|
305
320
|
} else {
|
|
306
321
|
// Reuse existing controllers for recursive calls
|
|
307
|
-
|
|
308
|
-
|
|
322
|
+
// Fallback to new controllers if they were cleared (should not happen in normal flow but good for tests)
|
|
323
|
+
abortController = this.abortController || new AbortController();
|
|
324
|
+
toolAbortController = this.toolAbortController || new AbortController();
|
|
309
325
|
}
|
|
310
326
|
|
|
327
|
+
// Save session in each recursion to ensure message persistence
|
|
328
|
+
await this.messageManager.saveSession();
|
|
329
|
+
|
|
311
330
|
// Only set loading state for the initial call
|
|
312
331
|
if (recursionDepth === 0) {
|
|
313
332
|
this.setIsLoading(true);
|
|
@@ -323,9 +342,7 @@ export class AIManager {
|
|
|
323
342
|
|
|
324
343
|
try {
|
|
325
344
|
// Get combined memory content
|
|
326
|
-
const combinedMemory = await
|
|
327
|
-
this.workdir,
|
|
328
|
-
);
|
|
345
|
+
const combinedMemory = await this.messageManager.getCombinedMemory();
|
|
329
346
|
|
|
330
347
|
// Track if assistant message has been created
|
|
331
348
|
let assistantMessageCreated = false;
|
|
@@ -342,6 +359,13 @@ export class AIManager {
|
|
|
342
359
|
toolsConfig,
|
|
343
360
|
);
|
|
344
361
|
|
|
362
|
+
// Inject language prompt if configured
|
|
363
|
+
const language = this.getLanguage();
|
|
364
|
+
if (language) {
|
|
365
|
+
const languagePrompt = `\n\n# Language\nAlways respond in ${language}. Technical terms (e.g., code, tool names, file paths) should remain in their original language or English where appropriate.`;
|
|
366
|
+
effectiveSystemPrompt = (effectiveSystemPrompt || "") + languagePrompt;
|
|
367
|
+
}
|
|
368
|
+
|
|
345
369
|
if (currentMode === "plan") {
|
|
346
370
|
const planFilePath = this.permissionManager?.getPlanFilePath();
|
|
347
371
|
if (planFilePath) {
|
|
@@ -601,8 +625,9 @@ export class AIManager {
|
|
|
601
625
|
// Create tool execution context
|
|
602
626
|
const context: ToolContext = {
|
|
603
627
|
abortSignal: toolAbortController.signal,
|
|
604
|
-
|
|
628
|
+
backgroundTaskManager: this.backgroundTaskManager,
|
|
605
629
|
workdir: this.workdir,
|
|
630
|
+
messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
|
|
606
631
|
};
|
|
607
632
|
|
|
608
633
|
// Execute tool
|
|
@@ -612,6 +637,23 @@ export class AIManager {
|
|
|
612
637
|
context,
|
|
613
638
|
);
|
|
614
639
|
|
|
640
|
+
// Check if the tool was backgrounded via Ctrl-B
|
|
641
|
+
// If it was backgrounded, we should abort the AI recursion
|
|
642
|
+
if (
|
|
643
|
+
toolResult.success &&
|
|
644
|
+
toolResult.content.includes(
|
|
645
|
+
"Command was manually backgrounded by user",
|
|
646
|
+
)
|
|
647
|
+
) {
|
|
648
|
+
this.logger?.info(
|
|
649
|
+
`Tool ${toolName} was backgrounded via Ctrl-B, aborting AI recursion`,
|
|
650
|
+
);
|
|
651
|
+
// Use abortAIMessage directly instead of abortRecursion to avoid double logging
|
|
652
|
+
// and ensure we don't trigger the "Request was aborted" error block
|
|
653
|
+
this.abortAIMessage();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
615
657
|
// Update message state - tool execution completed
|
|
616
658
|
this.messageManager.updateToolBlock({
|
|
617
659
|
id: toolId,
|
|
@@ -666,6 +708,15 @@ export class AIManager {
|
|
|
666
708
|
|
|
667
709
|
// Check if there are tool operations, if so automatically initiate next AI service call
|
|
668
710
|
if (toolCalls.length > 0) {
|
|
711
|
+
// Record committed snapshots to message history
|
|
712
|
+
if (this.reversionManager) {
|
|
713
|
+
const snapshots =
|
|
714
|
+
this.reversionManager.getAndClearCommittedSnapshots();
|
|
715
|
+
if (snapshots.length > 0) {
|
|
716
|
+
this.messageManager.addFileHistoryBlock(snapshots);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
669
720
|
// Check interruption status
|
|
670
721
|
const isCurrentlyAborted =
|
|
671
722
|
abortController.signal.aborted || toolAbortController.signal.aborted;
|
|
@@ -682,9 +733,18 @@ export class AIManager {
|
|
|
682
733
|
}
|
|
683
734
|
}
|
|
684
735
|
} catch (error) {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
736
|
+
// Check if the error is an abort error
|
|
737
|
+
// Use the local variables to avoid null reference if this.abortController was cleared
|
|
738
|
+
const isCurrentlyAborted =
|
|
739
|
+
abortController.signal.aborted || toolAbortController.signal.aborted;
|
|
740
|
+
|
|
741
|
+
if (isCurrentlyAborted) {
|
|
742
|
+
this.logger?.info("AI message processing was aborted");
|
|
743
|
+
} else {
|
|
744
|
+
this.messageManager.addErrorBlock(
|
|
745
|
+
error instanceof Error ? error.message : "Unknown error occurred",
|
|
746
|
+
);
|
|
747
|
+
}
|
|
688
748
|
} finally {
|
|
689
749
|
// Only execute cleanup and hooks for the initial call
|
|
690
750
|
if (recursionDepth === 0) {
|
|
@@ -705,6 +765,15 @@ export class AIManager {
|
|
|
705
765
|
abortController.signal.aborted || toolAbortController.signal.aborted;
|
|
706
766
|
|
|
707
767
|
if (!isCurrentlyAborted) {
|
|
768
|
+
// Record committed snapshots to message history for the final turn
|
|
769
|
+
if (this.reversionManager) {
|
|
770
|
+
const snapshots =
|
|
771
|
+
this.reversionManager.getAndClearCommittedSnapshots();
|
|
772
|
+
if (snapshots.length > 0) {
|
|
773
|
+
this.messageManager.addFileHistoryBlock(snapshots);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
708
777
|
const shouldContinue = await this.executeStopHooks();
|
|
709
778
|
|
|
710
779
|
// If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "child_process";
|
|
2
|
+
import { BackgroundTask, BackgroundShell } from "../types/processes.js";
|
|
3
|
+
import { stripAnsiColors } from "../utils/stringUtils.js";
|
|
4
|
+
import { logger } from "../utils/globalLogger.js";
|
|
5
|
+
|
|
6
|
+
export interface BackgroundTaskManagerCallbacks {
|
|
7
|
+
onTasksChange?: (tasks: BackgroundTask[]) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface BackgroundTaskManagerOptions {
|
|
11
|
+
callbacks?: BackgroundTaskManagerCallbacks;
|
|
12
|
+
workdir: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class BackgroundTaskManager {
|
|
16
|
+
private tasks = new Map<string, BackgroundTask>();
|
|
17
|
+
private nextId = 1;
|
|
18
|
+
private callbacks: BackgroundTaskManagerCallbacks;
|
|
19
|
+
private workdir: string;
|
|
20
|
+
|
|
21
|
+
constructor(options: BackgroundTaskManagerOptions) {
|
|
22
|
+
this.callbacks = options.callbacks || {};
|
|
23
|
+
this.workdir = options.workdir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private notifyTasksChange(): void {
|
|
27
|
+
this.callbacks.onTasksChange?.(Array.from(this.tasks.values()));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public generateId(): string {
|
|
31
|
+
return `task_${this.nextId++}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public addTask(task: BackgroundTask): void {
|
|
35
|
+
this.tasks.set(task.id, task);
|
|
36
|
+
this.notifyTasksChange();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public getTask(id: string): BackgroundTask | undefined {
|
|
40
|
+
return this.tasks.get(id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public getAllTasks(): BackgroundTask[] {
|
|
44
|
+
return Array.from(this.tasks.values());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public startShell(
|
|
48
|
+
command: string,
|
|
49
|
+
timeout?: number,
|
|
50
|
+
): { id: string; child: ChildProcess; detach: () => void } {
|
|
51
|
+
const id = this.generateId();
|
|
52
|
+
const startTime = Date.now();
|
|
53
|
+
|
|
54
|
+
const child = spawn(command, {
|
|
55
|
+
shell: true,
|
|
56
|
+
stdio: "pipe",
|
|
57
|
+
cwd: this.workdir,
|
|
58
|
+
env: {
|
|
59
|
+
...process.env,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const shell: BackgroundShell = {
|
|
64
|
+
id,
|
|
65
|
+
type: "shell",
|
|
66
|
+
process: child,
|
|
67
|
+
command,
|
|
68
|
+
startTime,
|
|
69
|
+
status: "running",
|
|
70
|
+
stdout: "",
|
|
71
|
+
stderr: "",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
this.tasks.set(id, shell);
|
|
75
|
+
this.notifyTasksChange();
|
|
76
|
+
|
|
77
|
+
// Set up timeout if specified
|
|
78
|
+
let timeoutHandle: NodeJS.Timeout | undefined;
|
|
79
|
+
if (timeout && timeout > 0) {
|
|
80
|
+
timeoutHandle = setTimeout(() => {
|
|
81
|
+
if (shell.status === "running") {
|
|
82
|
+
this.stopTask(id);
|
|
83
|
+
}
|
|
84
|
+
}, timeout);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const onStdout = (data: Buffer | string) => {
|
|
88
|
+
shell.stdout += stripAnsiColors(data.toString());
|
|
89
|
+
this.notifyTasksChange();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onStderr = (data: Buffer | string) => {
|
|
93
|
+
shell.stderr += stripAnsiColors(data.toString());
|
|
94
|
+
this.notifyTasksChange();
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const onExit = (code: number | null) => {
|
|
98
|
+
if (timeoutHandle) {
|
|
99
|
+
clearTimeout(timeoutHandle);
|
|
100
|
+
}
|
|
101
|
+
shell.status = code === 0 ? "completed" : "failed";
|
|
102
|
+
shell.exitCode = code ?? 0;
|
|
103
|
+
shell.endTime = Date.now();
|
|
104
|
+
shell.runtime = shell.endTime - startTime;
|
|
105
|
+
this.notifyTasksChange();
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const onError = (error: Error) => {
|
|
109
|
+
if (timeoutHandle) {
|
|
110
|
+
clearTimeout(timeoutHandle);
|
|
111
|
+
}
|
|
112
|
+
shell.status = "failed";
|
|
113
|
+
shell.stderr += `\nProcess error: ${stripAnsiColors(error.message)}`;
|
|
114
|
+
shell.exitCode = 1;
|
|
115
|
+
shell.endTime = Date.now();
|
|
116
|
+
shell.runtime = shell.endTime - startTime;
|
|
117
|
+
this.notifyTasksChange();
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
child.stdout?.on("data", onStdout);
|
|
121
|
+
child.stderr?.on("data", onStderr);
|
|
122
|
+
child.on("exit", onExit);
|
|
123
|
+
child.on("error", onError);
|
|
124
|
+
|
|
125
|
+
const detach = () => {
|
|
126
|
+
child.stdout?.off("data", onStdout);
|
|
127
|
+
child.stderr?.off("data", onStderr);
|
|
128
|
+
child.off("exit", onExit);
|
|
129
|
+
child.off("error", onError);
|
|
130
|
+
if (timeoutHandle) {
|
|
131
|
+
clearTimeout(timeoutHandle);
|
|
132
|
+
}
|
|
133
|
+
this.tasks.delete(id);
|
|
134
|
+
this.notifyTasksChange();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return { id, child, detach };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public adoptProcess(
|
|
141
|
+
child: ChildProcess,
|
|
142
|
+
command: string,
|
|
143
|
+
initialStdout: string = "",
|
|
144
|
+
initialStderr: string = "",
|
|
145
|
+
): string {
|
|
146
|
+
const id = this.generateId();
|
|
147
|
+
const startTime = Date.now();
|
|
148
|
+
|
|
149
|
+
const shell: BackgroundShell = {
|
|
150
|
+
id,
|
|
151
|
+
type: "shell",
|
|
152
|
+
process: child,
|
|
153
|
+
command,
|
|
154
|
+
startTime,
|
|
155
|
+
status: "running",
|
|
156
|
+
stdout: initialStdout,
|
|
157
|
+
stderr: initialStderr,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
this.tasks.set(id, shell);
|
|
161
|
+
this.notifyTasksChange();
|
|
162
|
+
|
|
163
|
+
child.stdout?.on("data", (data) => {
|
|
164
|
+
shell.stdout += stripAnsiColors(data.toString());
|
|
165
|
+
this.notifyTasksChange();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
child.stderr?.on("data", (data) => {
|
|
169
|
+
shell.stderr += stripAnsiColors(data.toString());
|
|
170
|
+
this.notifyTasksChange();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
child.on("exit", (code) => {
|
|
174
|
+
shell.status = code === 0 ? "completed" : "failed";
|
|
175
|
+
shell.exitCode = code ?? 0;
|
|
176
|
+
shell.endTime = Date.now();
|
|
177
|
+
shell.runtime = shell.endTime - startTime;
|
|
178
|
+
this.notifyTasksChange();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
child.on("error", (error) => {
|
|
182
|
+
shell.status = "failed";
|
|
183
|
+
shell.stderr += `\nProcess error: ${stripAnsiColors(error.message)}`;
|
|
184
|
+
shell.exitCode = 1;
|
|
185
|
+
shell.endTime = Date.now();
|
|
186
|
+
shell.runtime = shell.endTime - startTime;
|
|
187
|
+
this.notifyTasksChange();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return id;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public getOutput(
|
|
194
|
+
id: string,
|
|
195
|
+
filter?: string,
|
|
196
|
+
): { stdout: string; stderr: string; status: string } | null {
|
|
197
|
+
const task = this.tasks.get(id);
|
|
198
|
+
if (!task) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let stdout = task.stdout;
|
|
203
|
+
let stderr = task.stderr;
|
|
204
|
+
|
|
205
|
+
// Apply regex filter if provided
|
|
206
|
+
if (filter) {
|
|
207
|
+
try {
|
|
208
|
+
const regex = new RegExp(filter);
|
|
209
|
+
stdout = stdout
|
|
210
|
+
.split("\n")
|
|
211
|
+
.filter((line) => regex.test(line))
|
|
212
|
+
.join("\n");
|
|
213
|
+
stderr = stderr
|
|
214
|
+
.split("\n")
|
|
215
|
+
.filter((line) => regex.test(line))
|
|
216
|
+
.join("\n");
|
|
217
|
+
} catch (error) {
|
|
218
|
+
logger.warn(`Invalid filter regex: ${filter}`, error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
stdout,
|
|
224
|
+
stderr,
|
|
225
|
+
status: task.status,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
public stopTask(id: string): boolean {
|
|
230
|
+
const task = this.tasks.get(id);
|
|
231
|
+
if (!task || task.status !== "running") {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (task.type === "shell") {
|
|
236
|
+
const shell = task as BackgroundShell;
|
|
237
|
+
try {
|
|
238
|
+
// Try to kill process group first
|
|
239
|
+
if (shell.process.pid) {
|
|
240
|
+
process.kill(-shell.process.pid, "SIGTERM");
|
|
241
|
+
|
|
242
|
+
// Force kill after timeout
|
|
243
|
+
setTimeout(() => {
|
|
244
|
+
if (
|
|
245
|
+
shell.status === "running" &&
|
|
246
|
+
shell.process.pid &&
|
|
247
|
+
!shell.process.killed
|
|
248
|
+
) {
|
|
249
|
+
try {
|
|
250
|
+
process.kill(-shell.process.pid, "SIGKILL");
|
|
251
|
+
} catch (error) {
|
|
252
|
+
logger.error("Failed to force kill process:", error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}, 1000);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
shell.status = "killed";
|
|
259
|
+
shell.endTime = Date.now();
|
|
260
|
+
shell.runtime = shell.endTime - shell.startTime;
|
|
261
|
+
this.notifyTasksChange();
|
|
262
|
+
return true;
|
|
263
|
+
} catch {
|
|
264
|
+
// Fallback to direct process kill
|
|
265
|
+
try {
|
|
266
|
+
shell.process.kill("SIGTERM");
|
|
267
|
+
setTimeout(() => {
|
|
268
|
+
if (!shell.process.killed) {
|
|
269
|
+
shell.process.kill("SIGKILL");
|
|
270
|
+
}
|
|
271
|
+
}, 1000);
|
|
272
|
+
shell.status = "killed";
|
|
273
|
+
shell.endTime = Date.now();
|
|
274
|
+
shell.runtime = shell.endTime - shell.startTime;
|
|
275
|
+
this.notifyTasksChange();
|
|
276
|
+
return true;
|
|
277
|
+
} catch (directKillError) {
|
|
278
|
+
logger.error("Failed to kill child process:", directKillError);
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} else if (task.type === "subagent") {
|
|
283
|
+
// Subagent termination logic will be handled by aborting the AI loop
|
|
284
|
+
// which is already managed by the SubagentManager and AIManager.
|
|
285
|
+
// Here we just update the status.
|
|
286
|
+
task.status = "killed";
|
|
287
|
+
task.endTime = Date.now();
|
|
288
|
+
task.runtime = task.endTime - task.startTime;
|
|
289
|
+
this.notifyTasksChange();
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
public cleanup(): void {
|
|
297
|
+
// Kill all running tasks
|
|
298
|
+
for (const [id, task] of this.tasks) {
|
|
299
|
+
if (task.status === "running") {
|
|
300
|
+
this.stopTask(id);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
this.tasks.clear();
|
|
304
|
+
this.notifyTasksChange();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "child_process";
|
|
2
|
-
import { addBashCommandToHistory } from "../utils/bashHistory.js";
|
|
3
2
|
import type { MessageManager } from "./messageManager.js";
|
|
4
3
|
|
|
5
4
|
export interface BashManagerOptions {
|
|
@@ -66,9 +65,6 @@ export class BashManager {
|
|
|
66
65
|
child.on("exit", (code, signal) => {
|
|
67
66
|
const exitCode = code === null && signal ? 130 : (code ?? 0);
|
|
68
67
|
|
|
69
|
-
// Add command to bash history
|
|
70
|
-
addBashCommandToHistory(command, this.workdir);
|
|
71
|
-
|
|
72
68
|
this.messageManager.completeCommandMessage(command, exitCode);
|
|
73
69
|
|
|
74
70
|
this.setCommandRunning(false);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ForegroundTask, IForegroundTaskManager } from "../types/processes.js";
|
|
2
|
+
|
|
3
|
+
export class ForegroundTaskManager implements IForegroundTaskManager {
|
|
4
|
+
private activeForegroundTasks: ForegroundTask[] = [];
|
|
5
|
+
|
|
6
|
+
public registerForegroundTask(task: ForegroundTask): void {
|
|
7
|
+
this.activeForegroundTasks.push(task);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public unregisterForegroundTask(id: string): void {
|
|
11
|
+
this.activeForegroundTasks = this.activeForegroundTasks.filter(
|
|
12
|
+
(t) => t.id !== id,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public async backgroundCurrentTask(): Promise<void> {
|
|
17
|
+
const task = this.activeForegroundTasks.pop();
|
|
18
|
+
if (task) {
|
|
19
|
+
await task.backgroundHandler();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public hasActiveTasks(): boolean {
|
|
24
|
+
return this.activeForegroundTasks.length > 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { existsSync } from "fs";
|
|
11
11
|
import type { Logger } from "../types/index.js";
|
|
12
12
|
import type { PermissionMode } from "../types/permissions.js";
|
|
13
|
+
import type { Scope } from "../types/configuration.js";
|
|
13
14
|
import {
|
|
14
15
|
FileWatcherService,
|
|
15
16
|
type FileWatchEvent,
|
|
@@ -452,7 +453,7 @@ export class LiveConfigManager {
|
|
|
452
453
|
|
|
453
454
|
private async handleFileChange(
|
|
454
455
|
event: FileWatchEvent,
|
|
455
|
-
source:
|
|
456
|
+
source: Scope,
|
|
456
457
|
): Promise<void> {
|
|
457
458
|
this.logger?.debug(
|
|
458
459
|
`Live Config: File ${event.type} detected for ${source} config: ${event.path}`,
|
|
@@ -418,7 +418,9 @@ export class LspManager implements ILspManager {
|
|
|
418
418
|
await this.sendRequest(lspProc, "shutdown", {}, timeout);
|
|
419
419
|
await this.sendNotification(lspProc, "exit", {});
|
|
420
420
|
// Give it a moment to exit
|
|
421
|
-
|
|
421
|
+
if (timeout > 100) {
|
|
422
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
423
|
+
}
|
|
422
424
|
} catch (error) {
|
|
423
425
|
this.logger?.debug(
|
|
424
426
|
`Failed to gracefully shutdown LSP for ${language}: ${error}`,
|