wave-agent-sdk 0.13.5 → 0.14.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 +6 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +16 -2
- 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/messageManager.d.ts +15 -0
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +52 -2
- package/dist/managers/messageQueue.d.ts +1 -0
- package/dist/managers/messageQueue.d.ts.map +1 -1
- package/dist/managers/messageQueue.js +8 -0
- 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/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/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +11 -1
- 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/messaging.d.ts +1 -0
- package/dist/types/messaging.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 +17 -2
- package/src/managers/aiManager.ts +117 -15
- package/src/managers/messageManager.ts +64 -2
- package/src/managers/messageQueue.ts +9 -0
- package/src/managers/permissionManager.ts +7 -0
- package/src/managers/subagentManager.ts +28 -24
- package/src/prompts/index.ts +51 -25
- package/src/services/aiService.ts +14 -1
- 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/messaging.ts +1 -0
- 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
|
@@ -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();
|
|
@@ -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
|
}
|
|
@@ -29,4 +29,13 @@ export class MessageQueue {
|
|
|
29
29
|
getQueue(): QueuedMessage[] {
|
|
30
30
|
return [...this.queue];
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
removeAt(index: number): boolean {
|
|
34
|
+
if (index < 0 || index >= this.queue.length) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
this.queue.splice(index, 1);
|
|
38
|
+
this.onMessageEnqueued?.();
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
32
41
|
}
|
|
@@ -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
|
*/
|
|
@@ -567,23 +567,25 @@ export class SubagentManager {
|
|
|
567
567
|
instance.logStream?.end();
|
|
568
568
|
const task = backgroundTaskManager.getTask(instance.backgroundTaskId);
|
|
569
569
|
if (task) {
|
|
570
|
+
const wasAlreadyKilled = task.status === "killed";
|
|
570
571
|
task.status = "completed";
|
|
571
572
|
task.stdout = response || "Agent completed with no text response";
|
|
572
573
|
task.endTime = Date.now();
|
|
573
574
|
if (task.startTime) {
|
|
574
575
|
task.runtime = task.endTime - task.startTime;
|
|
575
576
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
577
|
+
// Skip notification if task was already stopped (e.g. by main agent shutdown)
|
|
578
|
+
if (!wasAlreadyKilled) {
|
|
579
|
+
const notificationQueue = this.container.has("NotificationQueue")
|
|
580
|
+
? this.container.get<NotificationQueue>("NotificationQueue")
|
|
581
|
+
: undefined;
|
|
582
|
+
if (notificationQueue) {
|
|
583
|
+
const summary = `Agent task "${instance.description}" completed`;
|
|
584
|
+
notificationQueue.enqueue(
|
|
585
|
+
`<task-notification>\n<task-id>${instance.backgroundTaskId}</task-id>\n<task-type>agent</task-type>\n<status>completed</status>\n<summary>${summary}</summary>\n</task-notification>`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
587
589
|
}
|
|
588
590
|
}
|
|
589
591
|
|
|
@@ -602,25 +604,27 @@ export class SubagentManager {
|
|
|
602
604
|
instance.logStream?.end();
|
|
603
605
|
const task = backgroundTaskManager.getTask(instance.backgroundTaskId);
|
|
604
606
|
if (task) {
|
|
607
|
+
const wasAlreadyKilled = task.status === "killed";
|
|
605
608
|
task.status = "failed";
|
|
606
609
|
task.stderr = error instanceof Error ? error.message : String(error);
|
|
607
610
|
task.endTime = Date.now();
|
|
608
611
|
if (task.startTime) {
|
|
609
612
|
task.runtime = task.endTime - task.startTime;
|
|
610
613
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
614
|
+
// Skip notification if task was already stopped (e.g. by main agent shutdown)
|
|
615
|
+
if (!wasAlreadyKilled) {
|
|
616
|
+
const notificationQueue = this.container.has("NotificationQueue")
|
|
617
|
+
? this.container.get<NotificationQueue>("NotificationQueue")
|
|
618
|
+
: undefined;
|
|
619
|
+
if (notificationQueue) {
|
|
620
|
+
const errorMsg =
|
|
621
|
+
error instanceof Error ? error.message : String(error);
|
|
622
|
+
const summary = `Agent task "${instance.description}" failed: ${errorMsg}`;
|
|
623
|
+
notificationQueue.enqueue(
|
|
624
|
+
`<task-notification>\n<task-id>${instance.backgroundTaskId}</task-id>\n<task-type>agent</task-type>\n<status>failed</status>\n<summary>${summary}</summary>\n</task-notification>`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
624
628
|
}
|
|
625
629
|
}
|
|
626
630
|
throw error;
|
package/src/prompts/index.ts
CHANGED
|
@@ -179,28 +179,52 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|
|
179
179
|
|
|
180
180
|
export const DEFAULT_SYSTEM_PROMPT = BASE_SYSTEM_PROMPT;
|
|
181
181
|
|
|
182
|
-
export const COMPRESS_MESSAGES_SYSTEM_PROMPT = `You
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
182
|
+
export const COMPRESS_MESSAGES_SYSTEM_PROMPT = `You are continuing work on a software engineering task. Write a detailed continuation summary that will allow you (or another instance of yourself) to resume work efficiently in a future context window where the conversation history will be replaced with this summary.
|
|
183
|
+
|
|
184
|
+
First, write your analysis in <analysis> tags as a thinking scratchpad:
|
|
185
|
+
- Chronologically review the conversation
|
|
186
|
+
- Identify user intents and goals
|
|
187
|
+
- Note files read/modified, approaches tried, decisions made
|
|
188
|
+
- Check for accuracy and completeness — ensure nothing critical is missing
|
|
189
|
+
|
|
190
|
+
Then produce a structured summary in <summary> tags with these sections:
|
|
191
|
+
|
|
192
|
+
## Primary Request and Intent
|
|
193
|
+
- The user's core request and success criteria
|
|
194
|
+
- Clarifications, constraints, or scope changes
|
|
195
|
+
|
|
196
|
+
## Key Technical Concepts
|
|
197
|
+
- Frameworks, libraries, patterns, architectural decisions
|
|
198
|
+
|
|
199
|
+
## Files and Code Sections
|
|
200
|
+
- Files read, modified, created (with full paths)
|
|
201
|
+
- Critical code snippets (function signatures, bug fixes, key logic)
|
|
202
|
+
- Focus on recent messages — include full code for important sections
|
|
203
|
+
|
|
204
|
+
## Errors and Fixes
|
|
205
|
+
- Errors encountered, root causes, how they were resolved
|
|
206
|
+
- Approaches tried that didn't work and why
|
|
207
|
+
|
|
208
|
+
## Problem Solving
|
|
209
|
+
- Approach evolution, trade-offs considered, decisions made
|
|
210
|
+
|
|
211
|
+
## All User Messages
|
|
212
|
+
- Complete list of all user messages (non-tool content)
|
|
213
|
+
- Preserve exact wording where load-bearing
|
|
214
|
+
|
|
215
|
+
## Pending Tasks
|
|
216
|
+
- Outstanding work, TODOs, unresolved questions
|
|
217
|
+
|
|
218
|
+
## Current Work
|
|
219
|
+
- What was being worked on at the time of summarization
|
|
220
|
+
- Exact state of in-progress changes
|
|
221
|
+
|
|
222
|
+
## Optional Next Step
|
|
223
|
+
- Immediate next action needed
|
|
224
|
+
- Include verbatim quotes from recent conversation if relevant
|
|
225
|
+
|
|
226
|
+
Be concise but complete — include information that prevents duplicate work or repeated mistakes.
|
|
227
|
+
Respond with text only. Do NOT call any tools.
|
|
204
228
|
Wrap your summary in <summary></summary> tags.`;
|
|
205
229
|
|
|
206
230
|
export const WEB_CONTENT_SYSTEM_PROMPT = `You are a helpful assistant that extracts information from web content. The content is provided in Markdown format.`;
|
|
@@ -214,6 +238,7 @@ export function buildSystemPrompt(
|
|
|
214
238
|
tools: ToolPlugin[],
|
|
215
239
|
options: {
|
|
216
240
|
workdir?: string;
|
|
241
|
+
originalWorkdir?: string;
|
|
217
242
|
memory?: string;
|
|
218
243
|
language?: string;
|
|
219
244
|
isSubagent?: boolean;
|
|
@@ -251,8 +276,9 @@ export function buildSystemPrompt(
|
|
|
251
276
|
prompt += `\n\n${buildPlanModePrompt(options.planMode.planFilePath, options.planMode.planExists, options.isSubagent)}`;
|
|
252
277
|
}
|
|
253
278
|
|
|
254
|
-
|
|
255
|
-
|
|
279
|
+
const workdirForPrompt = options.originalWorkdir || options.workdir;
|
|
280
|
+
if (workdirForPrompt) {
|
|
281
|
+
const isGitRepo = isGitRepository(workdirForPrompt);
|
|
256
282
|
const platform = os.platform();
|
|
257
283
|
const osVersion = `${os.type()} ${os.release()}`;
|
|
258
284
|
const today = new Date().toISOString().split("T")[0];
|
|
@@ -267,7 +293,7 @@ export function buildSystemPrompt(
|
|
|
267
293
|
|
|
268
294
|
Here is useful information about the environment you are running in:
|
|
269
295
|
<env>
|
|
270
|
-
Working directory: ${
|
|
296
|
+
Working directory: ${workdirForPrompt}
|
|
271
297
|
Is directory a git repo: ${isGitRepo}
|
|
272
298
|
Platform: ${platform}
|
|
273
299
|
Shell: ${shellName}
|
|
@@ -784,6 +784,19 @@ export async function compressMessages(
|
|
|
784
784
|
await acquireSlot(abortSignal);
|
|
785
785
|
}
|
|
786
786
|
|
|
787
|
+
// Strip images from messages before compact API call to reduce token usage
|
|
788
|
+
const cleanedMessages = messages.map((msg) => {
|
|
789
|
+
// Handle user/assistant messages with array content
|
|
790
|
+
if (Array.isArray(msg.content)) {
|
|
791
|
+
const textParts = msg.content.filter(
|
|
792
|
+
(part) => part.type === "text",
|
|
793
|
+
) as import("openai/resources.js").ChatCompletionContentPartText[];
|
|
794
|
+
const text = textParts.map((p) => p.text).join("\n");
|
|
795
|
+
return { ...msg, content: text || "(empty message)" };
|
|
796
|
+
}
|
|
797
|
+
return msg;
|
|
798
|
+
});
|
|
799
|
+
|
|
787
800
|
// Create OpenAI client with injected configuration
|
|
788
801
|
const openai = new OpenAIClient({
|
|
789
802
|
apiKey: gatewayConfig.apiKey,
|
|
@@ -821,7 +834,7 @@ export async function compressMessages(
|
|
|
821
834
|
role: "system",
|
|
822
835
|
content: COMPRESS_MESSAGES_SYSTEM_PROMPT,
|
|
823
836
|
},
|
|
824
|
-
...
|
|
837
|
+
...cleanedMessages,
|
|
825
838
|
{
|
|
826
839
|
role: "user",
|
|
827
840
|
content: `Please create a detailed summary of the conversation so far.`,
|
package/src/tools/agentTool.ts
CHANGED
|
@@ -71,7 +71,11 @@ When using the Agent tool, you must specify a subagent_type parameter to select
|
|
|
71
71
|
|
|
72
72
|
- When doing file search, prefer to use the ${AGENT_TOOL_NAME} tool in order to reduce context usage.
|
|
73
73
|
- You should proactively use the ${AGENT_TOOL_NAME} tool with specialized agents when the task at hand matches the agent's description.
|
|
74
|
-
- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the ${AGENT_TOOL_NAME} tool with subagent_type=${EXPLORE_SUBAGENT_TYPE} instead of running search commands directly
|
|
74
|
+
- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the ${AGENT_TOOL_NAME} tool with subagent_type=${EXPLORE_SUBAGENT_TYPE} instead of running search commands directly.
|
|
75
|
+
- You can optionally run agents in the background using the run_in_background parameter. When an agent runs in the background, you will be automatically notified when it completes — do NOT sleep, poll, or proactively check on its progress. Continue with other work or respond to the user instead.
|
|
76
|
+
- **Foreground vs background**: Use foreground (default) when you need the agent's results before you can proceed — e.g., research agents whose findings inform your next steps. Use background when you have genuinely independent work to do in parallel.
|
|
77
|
+
- **Don't peek.** The tool result includes an output file path — do not Read or tail it unless the user explicitly asks for a progress check. You get a completion notification; trust it. Reading the transcript mid-flight pulls the agent's tool noise into your context, which defeats the point of backgrounding.
|
|
78
|
+
- **Don't race.** After launching, you know nothing about what the agent found. Never fabricate or predict agent results in any format — not as prose, summary, or structured output. The notification arrives as a user-role message in a later turn; it is never something you write yourself. If the user asks a follow-up before the notification lands, tell them the agent is still running — give status, not a guess.`;
|
|
75
79
|
},
|
|
76
80
|
|
|
77
81
|
execute: async (
|
|
@@ -212,9 +216,17 @@ When using the Agent tool, you must specify a subagent_type parameter to select
|
|
|
212
216
|
if (run_in_background) {
|
|
213
217
|
const task = context.backgroundTaskManager?.getTask(result);
|
|
214
218
|
const outputPath = task?.outputPath;
|
|
219
|
+
const backgroundMsg = [
|
|
220
|
+
`Agent started in background with ID: ${result}.`,
|
|
221
|
+
`The agent is working in the background. You will be notified automatically when it completes.`,
|
|
222
|
+
`Do not duplicate this agent's work — avoid working with the same files or topics it is using.`,
|
|
223
|
+
outputPath
|
|
224
|
+
? `output_file: ${outputPath}`
|
|
225
|
+
: `Briefly tell the user what you launched and end your response.`,
|
|
226
|
+
].join("\n");
|
|
215
227
|
resolve({
|
|
216
228
|
success: true,
|
|
217
|
-
content:
|
|
229
|
+
content: backgroundMsg,
|
|
218
230
|
shortResult: `Agent started in background: ${result}`,
|
|
219
231
|
});
|
|
220
232
|
return;
|