wave-agent-sdk 0.13.6 → 0.14.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/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +4 -2
- package/dist/core/plugin.d.ts +2 -2
- package/dist/core/plugin.d.ts.map +1 -1
- package/dist/core/plugin.js +7 -7
- package/dist/managers/aiManager.d.ts +3 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +93 -8
- package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
- package/dist/managers/backgroundTaskManager.js +0 -12
- package/dist/managers/messageManager.d.ts +15 -0
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +52 -2
- package/dist/managers/permissionManager.d.ts +4 -0
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +6 -0
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +1 -1
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +23 -17
- package/dist/prompts/index.d.ts +2 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +50 -25
- package/dist/services/MarketplaceService.d.ts +53 -12
- package/dist/services/MarketplaceService.d.ts.map +1 -1
- package/dist/services/MarketplaceService.js +311 -123
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +11 -1
- package/dist/services/configurationService.d.ts +17 -1
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +104 -0
- package/dist/services/pluginLoader.d.ts +6 -0
- package/dist/services/pluginLoader.d.ts.map +1 -1
- package/dist/services/pluginLoader.js +52 -7
- package/dist/tools/agentTool.d.ts.map +1 -1
- package/dist/tools/agentTool.js +14 -2
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +27 -5
- package/dist/tools/types.d.ts +1 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/webFetchTool.d.ts.map +1 -1
- package/dist/tools/webFetchTool.js +202 -78
- package/dist/types/configuration.d.ts +7 -0
- package/dist/types/configuration.d.ts.map +1 -1
- package/dist/types/marketplace.d.ts +28 -1
- package/dist/types/marketplace.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +1 -0
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +13 -1
- package/dist/types/plugins.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +1 -1
- package/dist/utils/groupMessagesByApiRound.d.ts +24 -0
- package/dist/utils/groupMessagesByApiRound.d.ts.map +1 -0
- package/dist/utils/groupMessagesByApiRound.js +97 -0
- package/dist/utils/messageOperations.d.ts +1 -0
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/microcompact.d.ts +7 -0
- package/dist/utils/microcompact.d.ts.map +1 -0
- package/dist/utils/microcompact.js +78 -0
- package/package.json +2 -1
- package/src/agent.ts +4 -2
- package/src/core/plugin.ts +13 -7
- package/src/managers/aiManager.ts +117 -15
- package/src/managers/backgroundTaskManager.ts +1 -20
- package/src/managers/messageManager.ts +64 -2
- package/src/managers/permissionManager.ts +7 -0
- package/src/managers/pluginManager.ts +4 -1
- package/src/managers/subagentManager.ts +28 -24
- package/src/prompts/index.ts +51 -25
- package/src/services/MarketplaceService.ts +425 -134
- package/src/services/aiService.ts +14 -1
- package/src/services/configurationService.ts +131 -0
- package/src/services/pluginLoader.ts +66 -7
- package/src/tools/agentTool.ts +14 -2
- package/src/tools/bashTool.ts +27 -5
- package/src/tools/types.ts +1 -0
- package/src/tools/webFetchTool.ts +276 -86
- package/src/types/configuration.ts +8 -0
- package/src/types/marketplace.ts +26 -1
- package/src/types/messaging.ts +1 -0
- package/src/types/plugins.ts +13 -1
- package/src/utils/convertMessagesForAPI.ts +1 -1
- package/src/utils/groupMessagesByApiRound.ts +120 -0
- package/src/utils/messageOperations.ts +1 -0
- package/src/utils/microcompact.ts +101 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Message } from "../types/messaging.js";
|
|
2
|
+
export interface MicrocompactOptions {
|
|
3
|
+
timeThresholdMS: number;
|
|
4
|
+
recentResultsToKeep: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function microcompactMessages(messages: Message[], options: MicrocompactOptions): Message[];
|
|
7
|
+
//# sourceMappingURL=microcompact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"microcompact.d.ts","sourceRoot":"","sources":["../../src/utils/microcompact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,uBAAuB,CAAC;AAEhE,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAID,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,EAAE,CAwFX"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const CLEARED_RESULT = "[Old tool result content cleared]";
|
|
2
|
+
export function microcompactMessages(messages, options) {
|
|
3
|
+
const { timeThresholdMS, recentResultsToKeep } = options;
|
|
4
|
+
// 1. Find the latest tool block timestamp across all assistant messages
|
|
5
|
+
let lastAssistantTime = 0;
|
|
6
|
+
for (const msg of messages) {
|
|
7
|
+
if (msg.role === "assistant") {
|
|
8
|
+
for (const block of msg.blocks) {
|
|
9
|
+
if (block.type === "tool" && block.stage === "end" && block.timestamp) {
|
|
10
|
+
if (block.timestamp > lastAssistantTime) {
|
|
11
|
+
lastAssistantTime = block.timestamp;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// 2. If no prior assistant messages with completed tools, return unchanged
|
|
18
|
+
if (lastAssistantTime === 0) {
|
|
19
|
+
return messages;
|
|
20
|
+
}
|
|
21
|
+
// 3. If within threshold, return unchanged
|
|
22
|
+
if (Date.now() - lastAssistantTime < timeThresholdMS) {
|
|
23
|
+
return messages;
|
|
24
|
+
}
|
|
25
|
+
const toolRefs = [];
|
|
26
|
+
for (let mi = 0; mi < messages.length; mi++) {
|
|
27
|
+
const msg = messages[mi];
|
|
28
|
+
if (msg.role === "assistant") {
|
|
29
|
+
for (let bi = 0; bi < msg.blocks.length; bi++) {
|
|
30
|
+
const block = msg.blocks[bi];
|
|
31
|
+
if (block.type === "tool" && block.stage === "end" && block.timestamp) {
|
|
32
|
+
toolRefs.push({
|
|
33
|
+
msgIndex: mi,
|
|
34
|
+
blockIndex: bi,
|
|
35
|
+
timestamp: block.timestamp,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
toolRefs.sort((a, b) => b.timestamp - a.timestamp);
|
|
42
|
+
// 5. Mark the top N as "keep"
|
|
43
|
+
const keepSet = new Set();
|
|
44
|
+
for (let i = 0; i < Math.min(recentResultsToKeep, toolRefs.length); i++) {
|
|
45
|
+
const ref = toolRefs[i];
|
|
46
|
+
keepSet.add(`${ref.msgIndex}:${ref.blockIndex}`);
|
|
47
|
+
}
|
|
48
|
+
// 6. Deep-copy messages and clear result + shortResult on non-kept blocks
|
|
49
|
+
const result = messages.map((msg) => ({
|
|
50
|
+
...msg,
|
|
51
|
+
blocks: msg.blocks.map((block) => {
|
|
52
|
+
if (block.type === "tool" && block.stage === "end" && block.timestamp) {
|
|
53
|
+
return { ...block };
|
|
54
|
+
}
|
|
55
|
+
return block;
|
|
56
|
+
}),
|
|
57
|
+
}));
|
|
58
|
+
// Clear non-kept tool blocks
|
|
59
|
+
for (const ref of toolRefs) {
|
|
60
|
+
const key = `${ref.msgIndex}:${ref.blockIndex}`;
|
|
61
|
+
if (!keepSet.has(key)) {
|
|
62
|
+
result[ref.msgIndex] = {
|
|
63
|
+
...result[ref.msgIndex],
|
|
64
|
+
blocks: result[ref.msgIndex].blocks.map((b, idx) => {
|
|
65
|
+
if (idx === ref.blockIndex && b.type === "tool") {
|
|
66
|
+
return {
|
|
67
|
+
...b,
|
|
68
|
+
result: CLEARED_RESULT,
|
|
69
|
+
shortResult: undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return b;
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "SDK for building AI-powered development tools and agents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"cron-parser": "^5.5.0",
|
|
34
34
|
"fuzzysort": "^3.1.0",
|
|
35
35
|
"glob": "^13.0.0",
|
|
36
|
+
"lru-cache": "^11.3.5",
|
|
36
37
|
"minimatch": "^10.0.3",
|
|
37
38
|
"openai": "^5.12.2",
|
|
38
39
|
"turndown": "^7.2.2"
|
package/src/agent.ts
CHANGED
|
@@ -579,11 +579,13 @@ export class Agent {
|
|
|
579
579
|
|
|
580
580
|
/** Unified interrupt method, interrupts both AI messages and command execution */
|
|
581
581
|
public abortMessage(): void {
|
|
582
|
+
// Clear queue first to prevent processQueuedMessage from dequeuing
|
|
583
|
+
// when abortAIMessage triggers onLoadingChange(false)
|
|
584
|
+
this.messageQueue.clear();
|
|
585
|
+
this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
|
|
582
586
|
this.abortAIMessage(); // This will abort tools including Agent tool (subagents)
|
|
583
587
|
this.abortBashCommand();
|
|
584
588
|
this.abortSlashCommand();
|
|
585
|
-
this.messageQueue.clear();
|
|
586
|
-
this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
|
|
587
589
|
}
|
|
588
590
|
|
|
589
591
|
/** Interrupt bash command execution */
|
package/src/core/plugin.ts
CHANGED
|
@@ -30,7 +30,10 @@ export class PluginCore {
|
|
|
30
30
|
this.workdir = workdir;
|
|
31
31
|
this.container = new Container();
|
|
32
32
|
this.configurationService = new ConfigurationService();
|
|
33
|
-
this.marketplaceService = new MarketplaceService(
|
|
33
|
+
this.marketplaceService = new MarketplaceService(
|
|
34
|
+
this.workdir,
|
|
35
|
+
this.configurationService,
|
|
36
|
+
);
|
|
34
37
|
|
|
35
38
|
// Wire up ConfigurationService in the container for PluginManager to use
|
|
36
39
|
this.container.register("ConfigurationService", this.configurationService);
|
|
@@ -122,7 +125,7 @@ export class PluginCore {
|
|
|
122
125
|
for (const m of marketplaces) {
|
|
123
126
|
try {
|
|
124
127
|
const manifest = await this.marketplaceService.loadMarketplaceManifest(
|
|
125
|
-
this.marketplaceService.getMarketplacePath(m),
|
|
128
|
+
this.marketplaceService.getMarketplacePath(m.source),
|
|
126
129
|
);
|
|
127
130
|
manifest.plugins.forEach((p) => {
|
|
128
131
|
const pluginId = `${p.name}@${m.name}`;
|
|
@@ -154,15 +157,18 @@ export class PluginCore {
|
|
|
154
157
|
/**
|
|
155
158
|
* Adds a new marketplace
|
|
156
159
|
*/
|
|
157
|
-
async addMarketplace(
|
|
158
|
-
|
|
160
|
+
async addMarketplace(
|
|
161
|
+
input: string,
|
|
162
|
+
scope: Scope = "user",
|
|
163
|
+
): Promise<KnownMarketplace> {
|
|
164
|
+
return await this.marketplaceService.addMarketplace(input, scope);
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
/**
|
|
162
168
|
* Removes a marketplace by name
|
|
163
169
|
*/
|
|
164
|
-
async removeMarketplace(name: string): Promise<void> {
|
|
165
|
-
await this.marketplaceService.removeMarketplace(name);
|
|
170
|
+
async removeMarketplace(name: string, scope?: Scope): Promise<void> {
|
|
171
|
+
await this.marketplaceService.removeMarketplace(name, scope);
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
/**
|
|
@@ -208,7 +214,7 @@ export class PluginCore {
|
|
|
208
214
|
* Resolves the local path for a marketplace
|
|
209
215
|
*/
|
|
210
216
|
getMarketplacePath(marketplace: KnownMarketplace): string {
|
|
211
|
-
return this.marketplaceService.getMarketplacePath(marketplace);
|
|
217
|
+
return this.marketplaceService.getMarketplacePath(marketplace.source);
|
|
212
218
|
}
|
|
213
219
|
|
|
214
220
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type CallAgentOptions } from "../services/aiService.js";
|
|
2
2
|
import * as aiService from "../services/aiService.js";
|
|
3
3
|
import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
|
|
4
|
+
import { microcompactMessages } from "../utils/microcompact.js";
|
|
4
5
|
import { parseTaskNotificationXml } from "../utils/notificationXml.js";
|
|
5
6
|
import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
|
|
6
7
|
import * as fs from "node:fs/promises";
|
|
@@ -15,7 +16,6 @@ import type { ToolManager } from "./toolManager.js";
|
|
|
15
16
|
import type { ToolContext, ToolResult } from "../tools/types.js";
|
|
16
17
|
import type { MessageManager } from "./messageManager.js";
|
|
17
18
|
import type { BackgroundTaskManager } from "./backgroundTaskManager.js";
|
|
18
|
-
import type { NotificationQueue } from "./notificationQueue.js";
|
|
19
19
|
import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
|
|
20
20
|
import type { HookManager } from "./hookManager.js";
|
|
21
21
|
import type { ExtendedHookExecutionContext } from "../types/hooks.js";
|
|
@@ -25,6 +25,7 @@ import type { SkillManager } from "./skillManager.js";
|
|
|
25
25
|
import { buildSystemPrompt } from "../prompts/index.js";
|
|
26
26
|
import { Container } from "../utils/container.js";
|
|
27
27
|
import { ConfigurationService } from "../services/configurationService.js";
|
|
28
|
+
import type { NotificationQueue } from "./notificationQueue.js";
|
|
28
29
|
|
|
29
30
|
import { logger } from "../utils/globalLogger.js";
|
|
30
31
|
|
|
@@ -51,11 +52,13 @@ export class AIManager {
|
|
|
51
52
|
onLoadingChange?: (loading: boolean) => void;
|
|
52
53
|
private toolAbortController: AbortController | null = null;
|
|
53
54
|
private workdir: string;
|
|
55
|
+
private originalWorkdir: string;
|
|
54
56
|
private systemPrompt?: string;
|
|
55
57
|
private subagentType?: string; // Store subagent type for hook context
|
|
56
58
|
private stream: boolean; // Streaming mode flag
|
|
57
59
|
private modelOverride?: string;
|
|
58
60
|
private _onCwdChange?: (newCwd: string) => void; // Store callback for CWD changes
|
|
61
|
+
private consecutiveCompressionFailures: number = 0;
|
|
59
62
|
|
|
60
63
|
// Service overrides
|
|
61
64
|
constructor(
|
|
@@ -63,6 +66,7 @@ export class AIManager {
|
|
|
63
66
|
options: AIManagerOptions,
|
|
64
67
|
) {
|
|
65
68
|
this.workdir = options.workdir;
|
|
69
|
+
this.originalWorkdir = options.workdir;
|
|
66
70
|
this.systemPrompt = options.systemPrompt;
|
|
67
71
|
this.subagentType = options.subagentType; // Store subagent type
|
|
68
72
|
this.stream = options.stream ?? true; // Default to true if not specified
|
|
@@ -165,6 +169,10 @@ export class AIManager {
|
|
|
165
169
|
return this.workdir;
|
|
166
170
|
}
|
|
167
171
|
|
|
172
|
+
public getOriginalWorkdir(): string {
|
|
173
|
+
return this.originalWorkdir;
|
|
174
|
+
}
|
|
175
|
+
|
|
168
176
|
public setOnCwdChange(callback: (newCwd: string) => void): void {
|
|
169
177
|
this._onCwdChange = callback;
|
|
170
178
|
}
|
|
@@ -234,6 +242,7 @@ export class AIManager {
|
|
|
234
242
|
if (toolPlugin?.formatCompactParams) {
|
|
235
243
|
const context: ToolContext = {
|
|
236
244
|
workdir: this.workdir,
|
|
245
|
+
originalWorkdir: this.originalWorkdir,
|
|
237
246
|
taskManager: this.taskManager,
|
|
238
247
|
};
|
|
239
248
|
return toolPlugin.formatCompactParams(toolArgs, context);
|
|
@@ -248,7 +257,6 @@ export class AIManager {
|
|
|
248
257
|
private async handleTokenUsageAndCompression(
|
|
249
258
|
usage: Usage | undefined,
|
|
250
259
|
abortController: AbortController,
|
|
251
|
-
model?: string,
|
|
252
260
|
): Promise<void> {
|
|
253
261
|
if (!usage) return;
|
|
254
262
|
|
|
@@ -272,6 +280,14 @@ export class AIManager {
|
|
|
272
280
|
|
|
273
281
|
// If there are messages to compress, perform compression
|
|
274
282
|
if (messagesToCompress.length > 0) {
|
|
283
|
+
// Circuit breaker: skip compression after 3 consecutive failures
|
|
284
|
+
if (this.consecutiveCompressionFailures >= 3) {
|
|
285
|
+
logger?.warn(
|
|
286
|
+
`Skipping compression: ${this.consecutiveCompressionFailures} consecutive failures`,
|
|
287
|
+
);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
275
291
|
const recentChatMessages = convertMessagesForAPI(messagesToCompress);
|
|
276
292
|
|
|
277
293
|
// Save session before compression to preserve original messages
|
|
@@ -284,7 +300,7 @@ export class AIManager {
|
|
|
284
300
|
modelConfig: this.getModelConfig(),
|
|
285
301
|
messages: recentChatMessages,
|
|
286
302
|
abortSignal: abortController.signal,
|
|
287
|
-
model:
|
|
303
|
+
model: this.getModelConfig().fastModel,
|
|
288
304
|
});
|
|
289
305
|
|
|
290
306
|
// Handle usage tracking for compression operations
|
|
@@ -294,14 +310,91 @@ export class AIManager {
|
|
|
294
310
|
prompt_tokens: compressionResult.usage.prompt_tokens,
|
|
295
311
|
completion_tokens: compressionResult.usage.completion_tokens,
|
|
296
312
|
total_tokens: compressionResult.usage.total_tokens,
|
|
297
|
-
model:
|
|
313
|
+
model: this.getModelConfig().fastModel,
|
|
298
314
|
operation_type: "compress",
|
|
299
315
|
};
|
|
300
316
|
}
|
|
301
317
|
|
|
318
|
+
// Build post-compact context restoration
|
|
319
|
+
const POST_COMPACT_TOKEN_BUDGET = 50_000;
|
|
320
|
+
const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000;
|
|
321
|
+
const POST_COMPACT_MAX_FILES_TO_RESTORE = 5;
|
|
322
|
+
const contextParts: string[] = [];
|
|
323
|
+
|
|
324
|
+
// 1. File context restoration
|
|
325
|
+
const recentFiles = this.messageManager.getRecentFileReads(
|
|
326
|
+
POST_COMPACT_MAX_FILES_TO_RESTORE,
|
|
327
|
+
POST_COMPACT_MAX_TOKENS_PER_FILE,
|
|
328
|
+
);
|
|
329
|
+
let usedTokens = 0;
|
|
330
|
+
for (const file of recentFiles) {
|
|
331
|
+
const fileTokens = Math.ceil(file.content.length / 4);
|
|
332
|
+
if (usedTokens + fileTokens > POST_COMPACT_MAX_TOKENS_PER_FILE)
|
|
333
|
+
continue;
|
|
334
|
+
if (fileTokens > 0) usedTokens += fileTokens;
|
|
335
|
+
contextParts.push(
|
|
336
|
+
`\n\n## ${file.path}\n\`\`\`\n${file.content}\n\`\`\``,
|
|
337
|
+
);
|
|
338
|
+
if (contextParts.length >= POST_COMPACT_MAX_FILES_TO_RESTORE) break;
|
|
339
|
+
if (usedTokens >= POST_COMPACT_TOKEN_BUDGET) break;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 2. Working directory
|
|
343
|
+
contextParts.push(
|
|
344
|
+
`\n\n[Working Directory]\nCurrent working directory: ${this.workdir}`,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// 3. Plan mode context
|
|
348
|
+
const currentMode = this.permissionManager?.getCurrentEffectiveMode(
|
|
349
|
+
this.getModelConfig().permissionMode,
|
|
350
|
+
);
|
|
351
|
+
if (currentMode === "plan") {
|
|
352
|
+
const planFilePath = this.permissionManager?.getPlanFilePath();
|
|
353
|
+
if (planFilePath) {
|
|
354
|
+
let planExists = false;
|
|
355
|
+
try {
|
|
356
|
+
await fs.access(planFilePath);
|
|
357
|
+
planExists = true;
|
|
358
|
+
} catch {
|
|
359
|
+
// Plan file doesn't exist yet
|
|
360
|
+
}
|
|
361
|
+
contextParts.push(
|
|
362
|
+
`\n\n[Plan Mode]\nYou are in plan mode. Plan file: ${planFilePath} (exists: ${planExists})`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 4. Skills context
|
|
368
|
+
const skills =
|
|
369
|
+
this.skillManager
|
|
370
|
+
?.getAvailableSkills()
|
|
371
|
+
.filter((s) => !s.disableModelInvocation) || [];
|
|
372
|
+
if (skills.length > 0) {
|
|
373
|
+
const skillList = skills
|
|
374
|
+
.map((s) => `- ${s.name}: ${s.description || ""}`)
|
|
375
|
+
.join("\n");
|
|
376
|
+
contextParts.push(`\n\n[Available Skills]\n${skillList}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 5. Background agents status
|
|
380
|
+
const agents = this.backgroundTaskManager?.getAllTasks() || [];
|
|
381
|
+
if (agents.length > 0) {
|
|
382
|
+
const agentList = agents
|
|
383
|
+
.map((a) => `- Agent "${a.description}": ${a.status}`)
|
|
384
|
+
.join("\n");
|
|
385
|
+
contextParts.push(`\n\n[Background Tasks]\n${agentList}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Merge context restoration into summary
|
|
389
|
+
const enhancedSummary =
|
|
390
|
+
compressionResult.content +
|
|
391
|
+
(contextParts.length > 0
|
|
392
|
+
? `\n\n[Context Restoration]` + contextParts.join("")
|
|
393
|
+
: "");
|
|
394
|
+
|
|
302
395
|
// Execute message reconstruction and sessionId update after compression
|
|
303
396
|
this.messageManager.compressMessagesAndUpdateSession(
|
|
304
|
-
|
|
397
|
+
enhancedSummary,
|
|
305
398
|
compressionUsage,
|
|
306
399
|
);
|
|
307
400
|
|
|
@@ -313,8 +406,13 @@ export class AIManager {
|
|
|
313
406
|
logger?.debug(
|
|
314
407
|
`Successfully compressed ${messagesToCompress.length} messages and updated session`,
|
|
315
408
|
);
|
|
409
|
+
this.consecutiveCompressionFailures = 0;
|
|
316
410
|
} catch (compressError) {
|
|
317
|
-
|
|
411
|
+
this.consecutiveCompressionFailures++;
|
|
412
|
+
logger?.error(
|
|
413
|
+
`Failed to compress messages (${this.consecutiveCompressionFailures} consecutive):`,
|
|
414
|
+
compressError,
|
|
415
|
+
);
|
|
318
416
|
this.messageManager.addErrorBlock(
|
|
319
417
|
`Failed to compress conversation history: ${compressError instanceof Error ? compressError.message : String(compressError)}. You may encounter context limit issues.`,
|
|
320
418
|
);
|
|
@@ -403,10 +501,13 @@ export class AIManager {
|
|
|
403
501
|
toolAbortController = this.toolAbortController!;
|
|
404
502
|
}
|
|
405
503
|
|
|
406
|
-
// Get recent message history
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
504
|
+
// Get recent message history with microcompact applied
|
|
505
|
+
const rawMessages = this.messageManager.getMessages();
|
|
506
|
+
const microcompactedMessages = microcompactMessages(rawMessages, {
|
|
507
|
+
timeThresholdMS: 30 * 60 * 1000, // 30 minutes
|
|
508
|
+
recentResultsToKeep: 3,
|
|
509
|
+
});
|
|
510
|
+
const recentMessages = convertMessagesForAPI(microcompactedMessages);
|
|
410
511
|
|
|
411
512
|
try {
|
|
412
513
|
// Get combined memory content
|
|
@@ -472,6 +573,7 @@ export class AIManager {
|
|
|
472
573
|
filteredToolPlugins,
|
|
473
574
|
{
|
|
474
575
|
workdir: this.workdir,
|
|
576
|
+
originalWorkdir: this.originalWorkdir,
|
|
475
577
|
memory: combinedMemory,
|
|
476
578
|
language: this.getLanguage(),
|
|
477
579
|
isSubagent: !!this.subagentType,
|
|
@@ -658,6 +760,7 @@ export class AIManager {
|
|
|
658
760
|
stage: "end",
|
|
659
761
|
name: toolName,
|
|
660
762
|
compactParams: "",
|
|
763
|
+
timestamp: Date.now(),
|
|
661
764
|
});
|
|
662
765
|
return;
|
|
663
766
|
}
|
|
@@ -710,6 +813,7 @@ export class AIManager {
|
|
|
710
813
|
abortSignal: toolAbortController.signal,
|
|
711
814
|
backgroundTaskManager: this.backgroundTaskManager,
|
|
712
815
|
workdir: this.workdir,
|
|
816
|
+
originalWorkdir: this.originalWorkdir,
|
|
713
817
|
messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
|
|
714
818
|
sessionId: this.messageManager.getSessionId(),
|
|
715
819
|
toolCallId: toolId,
|
|
@@ -774,6 +878,7 @@ export class AIManager {
|
|
|
774
878
|
shortResult: toolResult.shortResult,
|
|
775
879
|
isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
|
|
776
880
|
startLineNumber: toolResult.startLineNumber,
|
|
881
|
+
timestamp: Date.now(),
|
|
777
882
|
});
|
|
778
883
|
|
|
779
884
|
// Execute PostToolUse hooks after successful tool completion
|
|
@@ -799,6 +904,7 @@ export class AIManager {
|
|
|
799
904
|
name: toolName,
|
|
800
905
|
compactParams,
|
|
801
906
|
isManuallyBackgrounded: false,
|
|
907
|
+
timestamp: Date.now(),
|
|
802
908
|
});
|
|
803
909
|
}
|
|
804
910
|
},
|
|
@@ -809,11 +915,7 @@ export class AIManager {
|
|
|
809
915
|
}
|
|
810
916
|
|
|
811
917
|
// Handle token statistics and message compression
|
|
812
|
-
await this.handleTokenUsageAndCompression(
|
|
813
|
-
result.usage,
|
|
814
|
-
abortController,
|
|
815
|
-
model,
|
|
816
|
-
);
|
|
918
|
+
await this.handleTokenUsageAndCompression(result.usage, abortController);
|
|
817
919
|
|
|
818
920
|
// Finalize text/reasoning blocks for the final response (no tools)
|
|
819
921
|
this.messageManager.finalizeStreamingBlocks();
|
|
@@ -2,11 +2,7 @@ import { spawn, type ChildProcess } from "child_process";
|
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
|
-
import {
|
|
6
|
-
BackgroundTask,
|
|
7
|
-
BackgroundShell,
|
|
8
|
-
BackgroundSubagent,
|
|
9
|
-
} from "../types/processes.js";
|
|
5
|
+
import { BackgroundTask, BackgroundShell } from "../types/processes.js";
|
|
10
6
|
import { stripAnsiColors } from "../utils/stringUtils.js";
|
|
11
7
|
import { logger } from "../utils/globalLogger.js";
|
|
12
8
|
import { Container } from "../utils/container.js";
|
|
@@ -427,21 +423,6 @@ export class BackgroundTaskManager {
|
|
|
427
423
|
task.runtime = task.endTime - task.startTime;
|
|
428
424
|
this.notifyTasksChange();
|
|
429
425
|
|
|
430
|
-
// Enqueue killed notification
|
|
431
|
-
const notificationQueue = this.container.has("NotificationQueue")
|
|
432
|
-
? this.container.get<NotificationQueue>("NotificationQueue")
|
|
433
|
-
: undefined;
|
|
434
|
-
if (notificationQueue) {
|
|
435
|
-
const description = (task as BackgroundSubagent).description || "";
|
|
436
|
-
const command = (task as BackgroundShell).command || "";
|
|
437
|
-
const summary =
|
|
438
|
-
task.type === "subagent"
|
|
439
|
-
? `Agent task "${description}" was stopped`
|
|
440
|
-
: `Command "${command}" was stopped`;
|
|
441
|
-
notificationQueue.enqueue(
|
|
442
|
-
`<task-notification>\n<task-id>${id}</task-id>\n<task-type>${task.type}</task-type>\n<status>killed</status>\n<summary>${summary}</summary>\n</task-notification>`,
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
426
|
return true;
|
|
446
427
|
}
|
|
447
428
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
generateMessageId,
|
|
17
17
|
} from "../utils/messageOperations.js";
|
|
18
18
|
import type { Message, Usage } from "../types/index.js";
|
|
19
|
+
import { getLastApiRounds } from "../utils/groupMessagesByApiRound.js";
|
|
19
20
|
import { join, isAbsolute, relative } from "path";
|
|
20
21
|
import {
|
|
21
22
|
appendMessages,
|
|
@@ -89,6 +90,8 @@ export class MessageManager {
|
|
|
89
90
|
private transcriptPath: string; // Cached transcript path
|
|
90
91
|
private savedMessageCount: number; // Track how many messages have been saved to prevent duplication
|
|
91
92
|
private filesInContext: Set<string> = new Set(); // Track files mentioned in the conversation
|
|
93
|
+
private recentFileReads: Map<string, { content: string; timestamp: number }> =
|
|
94
|
+
new Map(); // Track file read contents
|
|
92
95
|
private sessionType: "main" | "subagent";
|
|
93
96
|
private subagentType?: string;
|
|
94
97
|
private _usages: Usage[] = [];
|
|
@@ -266,11 +269,13 @@ export class MessageManager {
|
|
|
266
269
|
const newMessages = messages.slice(oldLength);
|
|
267
270
|
for (const message of newMessages) {
|
|
268
271
|
this.addPathsFromMessage(message);
|
|
272
|
+
this.extractFileReadsFromMessage(message);
|
|
269
273
|
}
|
|
270
274
|
|
|
271
275
|
// Also check if the last message was updated (common for tool blocks)
|
|
272
276
|
if (messages.length > 0 && messages.length === oldLength) {
|
|
273
277
|
this.addPathsFromMessage(messages[messages.length - 1]);
|
|
278
|
+
this.extractFileReadsFromMessage(messages[messages.length - 1]);
|
|
274
279
|
}
|
|
275
280
|
|
|
276
281
|
this.callbacks.onMessagesChange?.([...messages]);
|
|
@@ -495,8 +500,8 @@ export class MessageManager {
|
|
|
495
500
|
compressedContent: string,
|
|
496
501
|
usage?: Usage,
|
|
497
502
|
): void {
|
|
498
|
-
// Get last
|
|
499
|
-
const lastThreeMessages = this.messages
|
|
503
|
+
// Get last 2 API rounds to preserve (structurally safe boundary)
|
|
504
|
+
const lastThreeMessages = getLastApiRounds(this.messages, 2);
|
|
500
505
|
|
|
501
506
|
// Create compressed message
|
|
502
507
|
const compressMessage: Message = {
|
|
@@ -994,4 +999,61 @@ export class MessageManager {
|
|
|
994
999
|
|
|
995
1000
|
return paths;
|
|
996
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Extract file read contents from tool result blocks in a message.
|
|
1005
|
+
*/
|
|
1006
|
+
private extractFileReadsFromMessage(message: Message): void {
|
|
1007
|
+
for (const block of message.blocks) {
|
|
1008
|
+
if (
|
|
1009
|
+
block.type === "tool" &&
|
|
1010
|
+
block.name === "read" &&
|
|
1011
|
+
block.stage === "end" &&
|
|
1012
|
+
block.result &&
|
|
1013
|
+
block.parameters
|
|
1014
|
+
) {
|
|
1015
|
+
let filePath: string | undefined;
|
|
1016
|
+
try {
|
|
1017
|
+
const params = JSON.parse(block.parameters) as Record<
|
|
1018
|
+
string,
|
|
1019
|
+
unknown
|
|
1020
|
+
>;
|
|
1021
|
+
filePath = params.file_path as string | undefined;
|
|
1022
|
+
} catch {
|
|
1023
|
+
// Ignore parse errors
|
|
1024
|
+
}
|
|
1025
|
+
if (filePath) {
|
|
1026
|
+
this.recentFileReads.set(filePath, {
|
|
1027
|
+
content: block.result,
|
|
1028
|
+
timestamp: Date.now(),
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Get recent file read contents, sorted by timestamp (newest first).
|
|
1037
|
+
* @param maxFiles - Maximum number of files to return
|
|
1038
|
+
* @param maxTokensPerFile - Maximum tokens per file (~4 chars/token)
|
|
1039
|
+
* @returns Array of { path, content } sorted by recency
|
|
1040
|
+
*/
|
|
1041
|
+
public getRecentFileReads(
|
|
1042
|
+
maxFiles = 5,
|
|
1043
|
+
maxTokensPerFile = 5000,
|
|
1044
|
+
): Array<{ path: string; content: string }> {
|
|
1045
|
+
const sorted = Array.from(this.recentFileReads.entries())
|
|
1046
|
+
.sort(([, a], [, b]) => b.timestamp - a.timestamp)
|
|
1047
|
+
.slice(0, maxFiles);
|
|
1048
|
+
|
|
1049
|
+
const result: Array<{ path: string; content: string }> = [];
|
|
1050
|
+
for (const [path, { content }] of sorted) {
|
|
1051
|
+
const truncated =
|
|
1052
|
+
content.length > maxTokensPerFile * 4
|
|
1053
|
+
? content.slice(0, maxTokensPerFile * 4)
|
|
1054
|
+
: content;
|
|
1055
|
+
result.push({ path, content: truncated });
|
|
1056
|
+
}
|
|
1057
|
+
return result;
|
|
1058
|
+
}
|
|
997
1059
|
}
|
|
@@ -315,6 +315,13 @@ export class PermissionManager {
|
|
|
315
315
|
return this.planFilePath;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Public wrapper for isInsideSafeZone to check if a path is in the safe zone
|
|
320
|
+
*/
|
|
321
|
+
public isPathInSafeZone(targetPath: string): boolean {
|
|
322
|
+
return this.isInsideSafeZone(targetPath).isInside;
|
|
323
|
+
}
|
|
324
|
+
|
|
318
325
|
/**
|
|
319
326
|
* Check if a path is inside the Safe Zone (workdir + additionalDirectories)
|
|
320
327
|
*/
|
|
@@ -72,7 +72,10 @@ export class PluginManager {
|
|
|
72
72
|
);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
const marketplaceService = new MarketplaceService(
|
|
75
|
+
const marketplaceService = new MarketplaceService(
|
|
76
|
+
this.workdir,
|
|
77
|
+
this.configurationService,
|
|
78
|
+
);
|
|
76
79
|
|
|
77
80
|
// Trigger auto-update for marketplaces in the background
|
|
78
81
|
if (!process.env.VITEST) {
|