wave-agent-sdk 0.16.13 → 0.17.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/builtin/skills/settings/MCP.md +49 -4
- package/builtin/skills/settings/PERMISSIONS.md +31 -0
- package/dist/managers/aiManager.d.ts +19 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +335 -209
- package/dist/managers/hookManager.d.ts +22 -0
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +95 -17
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +49 -39
- package/dist/managers/messageManager.d.ts +4 -0
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +9 -0
- package/dist/managers/permissionManager.d.ts +6 -0
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +14 -0
- package/dist/managers/planManager.d.ts.map +1 -1
- package/dist/managers/planManager.js +10 -0
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +28 -3
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +14 -0
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +4 -0
- package/dist/prompts/index.d.ts +0 -4
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +0 -3
- package/dist/prompts/planModeReminders.d.ts +6 -0
- package/dist/prompts/planModeReminders.d.ts.map +1 -0
- package/dist/prompts/planModeReminders.js +112 -0
- package/dist/services/aiService.d.ts +1 -0
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +3 -1
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +5 -3
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +13 -12
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +6 -8
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +21 -8
- package/dist/tools/exitPlanMode.d.ts.map +1 -1
- package/dist/tools/exitPlanMode.js +2 -0
- package/dist/types/agent.d.ts +2 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +5 -1
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +2 -0
- package/dist/types/mcp.d.ts +1 -0
- package/dist/types/mcp.d.ts.map +1 -1
- package/dist/utils/editUtils.d.ts +3 -2
- package/dist/utils/editUtils.d.ts.map +1 -1
- package/dist/utils/editUtils.js +5 -3
- package/package.json +2 -2
- package/src/managers/aiManager.ts +416 -253
- package/src/managers/hookManager.ts +122 -18
- package/src/managers/mcpManager.ts +60 -47
- package/src/managers/messageManager.ts +10 -0
- package/src/managers/permissionManager.ts +18 -0
- package/src/managers/planManager.ts +11 -0
- package/src/managers/pluginManager.ts +52 -6
- package/src/managers/slashCommandManager.ts +17 -0
- package/src/managers/subagentManager.ts +4 -0
- package/src/prompts/index.ts +0 -8
- package/src/prompts/planModeReminders.ts +138 -0
- package/src/services/aiService.ts +4 -1
- package/src/services/configurationService.ts +5 -3
- package/src/services/initializationService.ts +16 -15
- package/src/tools/bashTool.ts +6 -7
- package/src/tools/editTool.ts +25 -8
- package/src/tools/exitPlanMode.ts +3 -0
- package/src/types/agent.ts +2 -0
- package/src/types/hooks.ts +9 -1
- package/src/types/mcp.ts +1 -0
- package/src/utils/editUtils.ts +6 -3
|
@@ -5,6 +5,7 @@ import { microcompactMessages } from "../utils/microcompact.js";
|
|
|
5
5
|
import { parseTaskNotificationXml } from "../utils/notificationXml.js";
|
|
6
6
|
import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
|
|
7
7
|
import * as fs from "node:fs/promises";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
8
9
|
import type {
|
|
9
10
|
GatewayConfig,
|
|
10
11
|
ModelConfig,
|
|
@@ -23,6 +24,12 @@ import type { PermissionManager } from "./permissionManager.js";
|
|
|
23
24
|
import type { SubagentManager } from "./subagentManager.js";
|
|
24
25
|
import type { SkillManager } from "./skillManager.js";
|
|
25
26
|
import { buildSystemPrompt } from "../prompts/index.js";
|
|
27
|
+
import {
|
|
28
|
+
buildPlanModeReminder,
|
|
29
|
+
buildPlanModeSparseReminder,
|
|
30
|
+
buildPlanModeReEntryReminder,
|
|
31
|
+
buildExitedPlanModeReminder,
|
|
32
|
+
} from "../prompts/planModeReminders.js";
|
|
26
33
|
import { Container } from "../utils/container.js";
|
|
27
34
|
import { recoverTruncatedJson } from "../utils/stringUtils.js";
|
|
28
35
|
import { ConfigurationService } from "../services/configurationService.js";
|
|
@@ -217,6 +224,119 @@ export class AIManager {
|
|
|
217
224
|
});
|
|
218
225
|
}
|
|
219
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Build plan mode system-reminder messages to inject into the API message stream.
|
|
229
|
+
* These are transient messages not stored in the message history.
|
|
230
|
+
* This preserves prompt caching by keeping the system prompt constant.
|
|
231
|
+
*/
|
|
232
|
+
private buildPlanModeMessages(
|
|
233
|
+
currentMode: PermissionMode | undefined,
|
|
234
|
+
): import("openai/resources.js").ChatCompletionMessageParam[] {
|
|
235
|
+
const messages: import("openai/resources.js").ChatCompletionMessageParam[] =
|
|
236
|
+
[];
|
|
237
|
+
if (!this.permissionManager) return messages;
|
|
238
|
+
|
|
239
|
+
// Handle exit notification (one-time after leaving plan mode)
|
|
240
|
+
if (this.permissionManager.getNeedsPlanModeExitAttachment()) {
|
|
241
|
+
const planFilePath = this.permissionManager.getPlanFilePath();
|
|
242
|
+
const planExists = planFilePath ? existsSync(planFilePath) : false;
|
|
243
|
+
messages.push({
|
|
244
|
+
role: "user",
|
|
245
|
+
content: buildExitedPlanModeReminder(planFilePath, planExists),
|
|
246
|
+
});
|
|
247
|
+
this.permissionManager.setNeedsPlanModeExitAttachment(false);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Handle plan mode reminders
|
|
251
|
+
if (currentMode !== "plan") return messages;
|
|
252
|
+
|
|
253
|
+
const planFilePath = this.permissionManager.getPlanFilePath();
|
|
254
|
+
if (!planFilePath) return messages;
|
|
255
|
+
|
|
256
|
+
const planExists = existsSync(planFilePath);
|
|
257
|
+
|
|
258
|
+
// Check for re-entry: flag is set AND plan file exists
|
|
259
|
+
if (this.permissionManager.hasExitedPlanModeInSession() && planExists) {
|
|
260
|
+
messages.push({
|
|
261
|
+
role: "user",
|
|
262
|
+
content: buildPlanModeReEntryReminder(planFilePath),
|
|
263
|
+
});
|
|
264
|
+
this.permissionManager.setHasExitedPlanMode(false); // One-time
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Count plan_mode system-reminders in existing messages to determine full vs sparse
|
|
268
|
+
// and count human turns since last reminder for throttling
|
|
269
|
+
const recentApiMessages = this.messageManager.getMessages();
|
|
270
|
+
let planModeReminderCount = 0;
|
|
271
|
+
let humanTurnsSinceLastReminder = 0;
|
|
272
|
+
let foundLastReminder = false;
|
|
273
|
+
|
|
274
|
+
for (let i = recentApiMessages.length - 1; i >= 0; i--) {
|
|
275
|
+
const msg = recentApiMessages[i];
|
|
276
|
+
if (msg.role === "user" && !msg.isMeta) {
|
|
277
|
+
// Count human turns (non-meta user messages without tool results)
|
|
278
|
+
const hasToolResult = msg.blocks?.some(
|
|
279
|
+
(b: { type: string }) => b.type === "tool",
|
|
280
|
+
);
|
|
281
|
+
if (!hasToolResult) {
|
|
282
|
+
if (!foundLastReminder) {
|
|
283
|
+
humanTurnsSinceLastReminder++;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Check for existing plan mode system-reminders
|
|
288
|
+
if (msg.role === "user" && msg.isMeta) {
|
|
289
|
+
const textContent = msg.blocks
|
|
290
|
+
?.filter((b) => b.type === "text")
|
|
291
|
+
.map((b) => ("content" in b ? b.content : ""))
|
|
292
|
+
.join("");
|
|
293
|
+
if (
|
|
294
|
+
textContent?.includes("Plan mode is active") ||
|
|
295
|
+
textContent?.includes("Plan mode still active")
|
|
296
|
+
) {
|
|
297
|
+
planModeReminderCount++;
|
|
298
|
+
if (!foundLastReminder) {
|
|
299
|
+
foundLastReminder = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Throttle: only inject every 5 human turns (but always inject on first turn)
|
|
306
|
+
const TURNS_BETWEEN_REMINDERS = 5;
|
|
307
|
+
const FULL_REMINDER_EVERY_N = 5;
|
|
308
|
+
|
|
309
|
+
if (
|
|
310
|
+
foundLastReminder &&
|
|
311
|
+
humanTurnsSinceLastReminder < TURNS_BETWEEN_REMINDERS
|
|
312
|
+
) {
|
|
313
|
+
return messages; // Throttled — skip reminder
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Determine full vs sparse
|
|
317
|
+
// Every 5th reminder is full; rest are sparse
|
|
318
|
+
const reminderNumber = planModeReminderCount + 1;
|
|
319
|
+
const isFull = reminderNumber % FULL_REMINDER_EVERY_N === 1;
|
|
320
|
+
|
|
321
|
+
if (isFull) {
|
|
322
|
+
messages.push({
|
|
323
|
+
role: "user",
|
|
324
|
+
content: buildPlanModeReminder(
|
|
325
|
+
planFilePath,
|
|
326
|
+
planExists,
|
|
327
|
+
!!this.subagentType,
|
|
328
|
+
),
|
|
329
|
+
});
|
|
330
|
+
} else {
|
|
331
|
+
messages.push({
|
|
332
|
+
role: "user",
|
|
333
|
+
content: buildPlanModeSparseReminder(planFilePath),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return messages;
|
|
338
|
+
}
|
|
339
|
+
|
|
220
340
|
public setIsLoading(isLoading: boolean): void {
|
|
221
341
|
this.isLoading = isLoading;
|
|
222
342
|
this.onLoadingChange?.(isLoading);
|
|
@@ -294,272 +414,327 @@ export class AIManager {
|
|
|
294
414
|
`Token usage exceeded ${this.getMaxInputTokens()}, compacting messages...`,
|
|
295
415
|
);
|
|
296
416
|
|
|
297
|
-
// Check if messages need compaction
|
|
298
417
|
const messagesToCompact = this.messageManager.getMessages();
|
|
418
|
+
if (messagesToCompact.length === 0) return;
|
|
299
419
|
|
|
300
|
-
//
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
420
|
+
// Circuit breaker: skip compaction after 3 consecutive failures
|
|
421
|
+
if (this.consecutiveCompactionFailures >= 3) {
|
|
422
|
+
logger?.warn(
|
|
423
|
+
`Skipping compaction: ${this.consecutiveCompactionFailures} consecutive failures`,
|
|
424
|
+
);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
309
427
|
|
|
310
|
-
|
|
428
|
+
await this.compactConversation({
|
|
429
|
+
abortSignal: abortController.signal,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
311
433
|
|
|
312
|
-
|
|
313
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Manually compact the conversation history.
|
|
436
|
+
* Called by /compact slash command or auto-compaction trigger.
|
|
437
|
+
*/
|
|
438
|
+
public async compactConversation(
|
|
439
|
+
options: {
|
|
440
|
+
customInstructions?: string;
|
|
441
|
+
abortSignal?: AbortSignal;
|
|
442
|
+
} = {},
|
|
443
|
+
): Promise<void> {
|
|
444
|
+
const messagesToCompact = this.messageManager.getMessages();
|
|
445
|
+
if (messagesToCompact.length === 0) {
|
|
446
|
+
logger?.debug("No messages to compact");
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
314
449
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
messages: recentChatMessages,
|
|
321
|
-
abortSignal: abortController.signal,
|
|
322
|
-
model: this.getModelConfig().fastModel,
|
|
323
|
-
});
|
|
450
|
+
// Circuit breaker: skip if already compacting
|
|
451
|
+
if (this.isCompacting) {
|
|
452
|
+
logger?.warn("Compaction already in progress");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
324
455
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
456
|
+
// 1. Run PreCompact hooks
|
|
457
|
+
let hookInstructions: string | undefined;
|
|
458
|
+
if (this.hookManager) {
|
|
459
|
+
try {
|
|
460
|
+
const preResult = await this.hookManager.executePreCompactHooks(
|
|
461
|
+
this.messageManager.getSessionId(),
|
|
462
|
+
this.messageManager.getTranscriptPath(),
|
|
463
|
+
options.customInstructions,
|
|
464
|
+
);
|
|
465
|
+
hookInstructions = preResult.additionalInstructions;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
logger?.warn(`PreCompact hooks failed: ${(error as Error).message}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
336
470
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
471
|
+
// 2. Merge custom instructions
|
|
472
|
+
const mergedInstructions =
|
|
473
|
+
[options.customInstructions, hookInstructions]
|
|
474
|
+
.filter(Boolean)
|
|
475
|
+
.join("\n") || undefined;
|
|
342
476
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
POST_COMPACT_MAX_FILES_TO_RESTORE,
|
|
346
|
-
POST_COMPACT_MAX_TOKENS_PER_FILE,
|
|
347
|
-
);
|
|
348
|
-
let usedTokens = 0;
|
|
349
|
-
for (const file of recentFiles) {
|
|
350
|
-
const fileTokens = Math.ceil(file.content.length / 4);
|
|
351
|
-
if (usedTokens + fileTokens > POST_COMPACT_MAX_TOKENS_PER_FILE)
|
|
352
|
-
continue;
|
|
353
|
-
if (fileTokens > 0) usedTokens += fileTokens;
|
|
354
|
-
contextParts.push(
|
|
355
|
-
`\n\n## ${file.path}\n\`\`\`\n${file.content}\n\`\`\``,
|
|
356
|
-
);
|
|
357
|
-
if (contextParts.length >= POST_COMPACT_MAX_FILES_TO_RESTORE) break;
|
|
358
|
-
if (usedTokens >= POST_COMPACT_TOKEN_BUDGET) break;
|
|
359
|
-
}
|
|
477
|
+
// 3. Save session before compaction
|
|
478
|
+
await this.messageManager.saveSession();
|
|
360
479
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
);
|
|
480
|
+
this.setIsCompacting(true);
|
|
481
|
+
try {
|
|
482
|
+
const recentChatMessages = convertMessagesForAPI(messagesToCompact);
|
|
365
483
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
await fs.access(planFilePath);
|
|
376
|
-
planExists = true;
|
|
377
|
-
} catch {
|
|
378
|
-
// Plan file doesn't exist yet
|
|
379
|
-
}
|
|
380
|
-
contextParts.push(
|
|
381
|
-
`\n\n[Plan Mode]\nYou are in plan mode. Plan file: ${planFilePath} (exists: ${planExists})`,
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
484
|
+
// 4. Call compactMessages with optional custom instructions
|
|
485
|
+
const compactResult = await aiService.compactMessages({
|
|
486
|
+
gatewayConfig: this.getGatewayConfig(),
|
|
487
|
+
modelConfig: this.getModelConfig(),
|
|
488
|
+
messages: recentChatMessages,
|
|
489
|
+
abortSignal: options.abortSignal,
|
|
490
|
+
model: this.getModelConfig().fastModel,
|
|
491
|
+
customInstructions: mergedInstructions,
|
|
492
|
+
});
|
|
385
493
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (!skill) continue;
|
|
494
|
+
// 5. Handle usage tracking
|
|
495
|
+
let compactUsage: Usage | undefined;
|
|
496
|
+
if (compactResult.usage) {
|
|
497
|
+
compactUsage = {
|
|
498
|
+
prompt_tokens: compactResult.usage.prompt_tokens,
|
|
499
|
+
completion_tokens: compactResult.usage.completion_tokens,
|
|
500
|
+
total_tokens: compactResult.usage.total_tokens,
|
|
501
|
+
model: this.getModelConfig().fastModel,
|
|
502
|
+
operation_type: "compact",
|
|
503
|
+
};
|
|
504
|
+
}
|
|
398
505
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
let skillContent = contentMatch
|
|
404
|
-
? contentMatch[1].trim()
|
|
405
|
-
: skill.content;
|
|
406
|
-
|
|
407
|
-
// Per-skill token budget enforcement (~4 chars per token)
|
|
408
|
-
const maxSkillChars = POST_COMPACT_MAX_TOKENS_PER_SKILL * 4;
|
|
409
|
-
if (skillContent.length > maxSkillChars) {
|
|
410
|
-
skillContent =
|
|
411
|
-
skillContent.slice(0, maxSkillChars) +
|
|
412
|
-
"\n\n...[truncated]...";
|
|
413
|
-
}
|
|
506
|
+
// 6. Build post-compact context restoration
|
|
507
|
+
const enhancedSummary = await this.buildPostCompactContext(
|
|
508
|
+
compactResult.content,
|
|
509
|
+
);
|
|
414
510
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
break;
|
|
421
|
-
skillsUsedTokens += skillTokens;
|
|
511
|
+
// 7. Execute message reconstruction
|
|
512
|
+
this.messageManager.compactMessagesAndUpdateSession(
|
|
513
|
+
enhancedSummary,
|
|
514
|
+
compactUsage,
|
|
515
|
+
);
|
|
422
516
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// Skip skills that can't be loaded
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
if (invokedSkillParts.length > 0) {
|
|
431
|
-
contextParts.push(
|
|
432
|
-
`\n\n[Invoked Skills]\n${invokedSkillParts.join("")}`,
|
|
433
|
-
);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
517
|
+
// 8. Track usage
|
|
518
|
+
if (compactUsage && this.callbacks?.onUsageAdded) {
|
|
519
|
+
this.callbacks.onUsageAdded(compactUsage);
|
|
520
|
+
}
|
|
436
521
|
|
|
437
|
-
|
|
438
|
-
const agents =
|
|
439
|
-
this.backgroundTaskManager
|
|
440
|
-
?.getAllTasks()
|
|
441
|
-
.filter((a) => a.type === "subagent") || [];
|
|
442
|
-
if (agents.length > 0) {
|
|
443
|
-
const agentParts: string[] = [];
|
|
444
|
-
for (const a of agents) {
|
|
445
|
-
if (a.status === "killed") {
|
|
446
|
-
agentParts.push(
|
|
447
|
-
`Task "${a.description}" (${a.id}) was stopped by the user.`,
|
|
448
|
-
);
|
|
449
|
-
} else if (a.status === "running") {
|
|
450
|
-
const parts = [
|
|
451
|
-
`Background agent "${a.description}" (${a.id}) is still running.`,
|
|
452
|
-
`Do NOT spawn a duplicate. You will be notified when it completes.`,
|
|
453
|
-
];
|
|
454
|
-
if (a.outputPath) {
|
|
455
|
-
parts.push(`You can read partial output at ${a.outputPath}.`);
|
|
456
|
-
}
|
|
457
|
-
agentParts.push(parts.join(" "));
|
|
458
|
-
} else {
|
|
459
|
-
// completed or failed
|
|
460
|
-
const parts = [
|
|
461
|
-
`Task ${a.id} (status: ${a.status}) (description: ${a.description}).`,
|
|
462
|
-
];
|
|
463
|
-
const deltaText = a.status === "failed" ? a.stderr : a.stdout;
|
|
464
|
-
if (deltaText && deltaText.length > 0) {
|
|
465
|
-
const summary =
|
|
466
|
-
deltaText.length > 500
|
|
467
|
-
? deltaText.slice(0, 500) + "..."
|
|
468
|
-
: deltaText;
|
|
469
|
-
parts.push(`Delta: ${summary}`);
|
|
470
|
-
}
|
|
471
|
-
if (a.outputPath) {
|
|
472
|
-
parts.push(
|
|
473
|
-
`Read the output file to retrieve the result: ${a.outputPath}.`,
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
agentParts.push(parts.join(" "));
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
if (agentParts.length > 0) {
|
|
480
|
-
contextParts.push(
|
|
481
|
-
`\n\n[Background Tasks]\n${agentParts.join("\n")}`,
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
522
|
+
this.consecutiveCompactionFailures = 0;
|
|
485
523
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
// Execute message reconstruction and sessionId update after compaction
|
|
494
|
-
this.messageManager.compactMessagesAndUpdateSession(
|
|
495
|
-
enhancedSummary,
|
|
496
|
-
compactUsage,
|
|
497
|
-
);
|
|
524
|
+
// 9. Log OTEL event
|
|
525
|
+
logOTelEvent("compaction", {
|
|
526
|
+
beforeTokens: String(messagesToCompact.length),
|
|
527
|
+
afterTokens: "1",
|
|
528
|
+
model: this.getModelConfig().fastModel,
|
|
529
|
+
}).catch(() => {});
|
|
498
530
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
531
|
+
// 10. Run SessionStart hooks (existing behavior)
|
|
532
|
+
if (this.hookManager) {
|
|
533
|
+
try {
|
|
534
|
+
const newSessionId = this.messageManager.getSessionId();
|
|
535
|
+
const sessionStartResult =
|
|
536
|
+
await this.hookManager.executeSessionStartHooks(
|
|
537
|
+
"compact",
|
|
538
|
+
newSessionId,
|
|
539
|
+
this.messageManager.getTranscriptPath(),
|
|
540
|
+
this.subagentType,
|
|
541
|
+
);
|
|
542
|
+
if (sessionStartResult.additionalContext) {
|
|
543
|
+
this.messageManager.addUserMessage({
|
|
544
|
+
content: `<system-reminder>\nSessionStart hook additional context: ${sessionStartResult.additionalContext}\n</system-reminder>`,
|
|
545
|
+
isMeta: true,
|
|
546
|
+
});
|
|
502
547
|
}
|
|
548
|
+
if (sessionStartResult.initialUserMessage) {
|
|
549
|
+
this.messageManager.addUserMessage({
|
|
550
|
+
content: sessionStartResult.initialUserMessage,
|
|
551
|
+
isMeta: true,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
} catch (error) {
|
|
555
|
+
logger?.warn(
|
|
556
|
+
`SessionStart hooks on compact failed: ${(error as Error).message}`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
503
560
|
|
|
504
|
-
|
|
505
|
-
|
|
561
|
+
// 11. Run PostCompact hooks
|
|
562
|
+
if (this.hookManager) {
|
|
563
|
+
try {
|
|
564
|
+
await this.hookManager.executePostCompactHooks(
|
|
565
|
+
this.messageManager.getSessionId(),
|
|
566
|
+
this.messageManager.getTranscriptPath(),
|
|
567
|
+
compactResult.content,
|
|
506
568
|
);
|
|
507
|
-
|
|
569
|
+
} catch (error) {
|
|
570
|
+
logger?.warn(`PostCompact hooks failed: ${(error as Error).message}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
508
573
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
574
|
+
logger?.debug(
|
|
575
|
+
`Successfully compacted ${messagesToCompact.length} messages`,
|
|
576
|
+
);
|
|
577
|
+
} catch (compactError) {
|
|
578
|
+
this.consecutiveCompactionFailures++;
|
|
579
|
+
logger?.error(
|
|
580
|
+
`Failed to compact messages (${this.consecutiveCompactionFailures} consecutive):`,
|
|
581
|
+
compactError,
|
|
582
|
+
);
|
|
583
|
+
this.messageManager.addErrorBlock(
|
|
584
|
+
`Failed to compact conversation history: ${compactError instanceof Error ? compactError.message : String(compactError)}. You may encounter context limit issues.`,
|
|
585
|
+
);
|
|
586
|
+
} finally {
|
|
587
|
+
this.setIsCompacting(false);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
515
590
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
591
|
+
/**
|
|
592
|
+
* Build post-compact context restoration content.
|
|
593
|
+
* Restores file reads, working directory, plan mode, skills, and background tasks.
|
|
594
|
+
*/
|
|
595
|
+
private async buildPostCompactContext(summary: string): Promise<string> {
|
|
596
|
+
const POST_COMPACT_TOKEN_BUDGET = 50_000;
|
|
597
|
+
const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000;
|
|
598
|
+
const POST_COMPACT_MAX_FILES_TO_RESTORE = 5;
|
|
599
|
+
const contextParts: string[] = [];
|
|
600
|
+
|
|
601
|
+
// 1. File context restoration
|
|
602
|
+
const recentFiles = this.messageManager.getRecentFileReads(
|
|
603
|
+
POST_COMPACT_MAX_FILES_TO_RESTORE,
|
|
604
|
+
POST_COMPACT_MAX_TOKENS_PER_FILE,
|
|
605
|
+
);
|
|
606
|
+
let usedTokens = 0;
|
|
607
|
+
for (const file of recentFiles) {
|
|
608
|
+
const fileTokens = Math.ceil(file.content.length / 4);
|
|
609
|
+
if (usedTokens + fileTokens > POST_COMPACT_MAX_TOKENS_PER_FILE) continue;
|
|
610
|
+
if (fileTokens > 0) usedTokens += fileTokens;
|
|
611
|
+
contextParts.push(`\n\n## ${file.path}\n\`\`\`\n${file.content}\n\`\`\``);
|
|
612
|
+
if (contextParts.length >= POST_COMPACT_MAX_FILES_TO_RESTORE) break;
|
|
613
|
+
if (usedTokens >= POST_COMPACT_TOKEN_BUDGET) break;
|
|
614
|
+
}
|
|
527
615
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
isMeta: true,
|
|
533
|
-
});
|
|
534
|
-
}
|
|
616
|
+
// 2. Working directory
|
|
617
|
+
contextParts.push(
|
|
618
|
+
`\n\n[Working Directory]\nCurrent working directory: ${this.getWorkdir()}`,
|
|
619
|
+
);
|
|
535
620
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
621
|
+
// 3. Plan mode context
|
|
622
|
+
const currentMode = this.permissionManager?.getCurrentEffectiveMode(
|
|
623
|
+
this.getModelConfig().permissionMode,
|
|
624
|
+
);
|
|
625
|
+
if (currentMode === "plan") {
|
|
626
|
+
const planFilePath = this.permissionManager?.getPlanFilePath();
|
|
627
|
+
if (planFilePath) {
|
|
628
|
+
let planExists = false;
|
|
629
|
+
try {
|
|
630
|
+
await fs.access(planFilePath);
|
|
631
|
+
planExists = true;
|
|
632
|
+
} catch {
|
|
633
|
+
// Plan file doesn't exist yet
|
|
634
|
+
}
|
|
635
|
+
contextParts.push(
|
|
636
|
+
`\n\n${buildPlanModeReminder(planFilePath, planExists, !!this.subagentType)}`,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 4. Invoked skills context (with token budget, matching Claude Code)
|
|
642
|
+
const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000;
|
|
643
|
+
const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000;
|
|
644
|
+
const invokedSkillNames = this.messageManager.getInvokedSkillNames(10);
|
|
645
|
+
if (invokedSkillNames.length > 0 && this.skillManager) {
|
|
646
|
+
const invokedSkillParts: string[] = [];
|
|
647
|
+
let skillsUsedTokens = 0;
|
|
648
|
+
for (const skillName of invokedSkillNames) {
|
|
649
|
+
try {
|
|
650
|
+
const skill = await this.skillManager.loadSkill(skillName);
|
|
651
|
+
if (!skill) continue;
|
|
652
|
+
|
|
653
|
+
const contentMatch = skill.content.match(
|
|
654
|
+
/^---\n[\s\S]*?\n---\n([\s\S]*)$/,
|
|
655
|
+
);
|
|
656
|
+
let skillContent = contentMatch
|
|
657
|
+
? contentMatch[1].trim()
|
|
658
|
+
: skill.content;
|
|
659
|
+
|
|
660
|
+
const maxSkillChars = POST_COMPACT_MAX_TOKENS_PER_SKILL * 4;
|
|
661
|
+
if (skillContent.length > maxSkillChars) {
|
|
662
|
+
skillContent =
|
|
663
|
+
skillContent.slice(0, maxSkillChars) + "\n\n...[truncated]...";
|
|
548
664
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
665
|
+
|
|
666
|
+
const skillTokens = Math.ceil(skillContent.length / 4);
|
|
667
|
+
if (skillsUsedTokens + skillTokens > POST_COMPACT_SKILLS_TOKEN_BUDGET)
|
|
668
|
+
break;
|
|
669
|
+
skillsUsedTokens += skillTokens;
|
|
670
|
+
|
|
671
|
+
invokedSkillParts.push(
|
|
672
|
+
`\n\n## ${skill.name}\n${skill.description ? `*${skill.description}*\n\n` : ""}\`\`\`\n${skillContent}\n\`\`\``,
|
|
554
673
|
);
|
|
555
|
-
|
|
556
|
-
|
|
674
|
+
} catch {
|
|
675
|
+
// Skip skills that can't be loaded
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (invokedSkillParts.length > 0) {
|
|
679
|
+
contextParts.push(
|
|
680
|
+
`\n\n[Invoked Skills]\n${invokedSkillParts.join("")}`,
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// 5. Background subagent status (shell tasks excluded, matching Claude Code's createAsyncAgentAttachmentsIfNeeded)
|
|
686
|
+
const agents =
|
|
687
|
+
this.backgroundTaskManager
|
|
688
|
+
?.getAllTasks()
|
|
689
|
+
.filter((a) => a.type === "subagent") || [];
|
|
690
|
+
if (agents.length > 0) {
|
|
691
|
+
const agentParts: string[] = [];
|
|
692
|
+
for (const a of agents) {
|
|
693
|
+
if (a.status === "killed") {
|
|
694
|
+
agentParts.push(
|
|
695
|
+
`Task "${a.description}" (${a.id}) was stopped by the user.`,
|
|
557
696
|
);
|
|
558
|
-
}
|
|
559
|
-
|
|
697
|
+
} else if (a.status === "running") {
|
|
698
|
+
const parts = [
|
|
699
|
+
`Background agent "${a.description}" (${a.id}) is still running.`,
|
|
700
|
+
`Do NOT spawn a duplicate. You will be notified when it completes.`,
|
|
701
|
+
];
|
|
702
|
+
if (a.outputPath) {
|
|
703
|
+
parts.push(`You can read partial output at ${a.outputPath}.`);
|
|
704
|
+
}
|
|
705
|
+
agentParts.push(parts.join(" "));
|
|
706
|
+
} else {
|
|
707
|
+
// completed or failed
|
|
708
|
+
const parts = [
|
|
709
|
+
`Task ${a.id} (status: ${a.status}) (description: ${a.description}).`,
|
|
710
|
+
];
|
|
711
|
+
const deltaText = a.status === "failed" ? a.stderr : a.stdout;
|
|
712
|
+
if (deltaText && deltaText.length > 0) {
|
|
713
|
+
const summary =
|
|
714
|
+
deltaText.length > 500
|
|
715
|
+
? deltaText.slice(0, 500) + "..."
|
|
716
|
+
: deltaText;
|
|
717
|
+
parts.push(`Delta: ${summary}`);
|
|
718
|
+
}
|
|
719
|
+
if (a.outputPath) {
|
|
720
|
+
parts.push(
|
|
721
|
+
`Read the output file to retrieve the result: ${a.outputPath}.`,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
agentParts.push(parts.join(" "));
|
|
560
725
|
}
|
|
561
726
|
}
|
|
727
|
+
if (agentParts.length > 0) {
|
|
728
|
+
contextParts.push(`\n\n[Background Tasks]\n${agentParts.join("\n")}`);
|
|
729
|
+
}
|
|
562
730
|
}
|
|
731
|
+
|
|
732
|
+
return (
|
|
733
|
+
summary +
|
|
734
|
+
(contextParts.length > 0
|
|
735
|
+
? `\n\n[Context Restoration]` + contextParts.join("")
|
|
736
|
+
: "")
|
|
737
|
+
);
|
|
563
738
|
}
|
|
564
739
|
|
|
565
740
|
public getIsCompacting(): boolean {
|
|
@@ -685,22 +860,11 @@ export class AIManager {
|
|
|
685
860
|
.getTools()
|
|
686
861
|
.filter((t) => toolNames.has(t.name));
|
|
687
862
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const planFilePath = this.permissionManager?.getPlanFilePath();
|
|
694
|
-
if (planFilePath) {
|
|
695
|
-
let planExists = false;
|
|
696
|
-
try {
|
|
697
|
-
await fs.access(planFilePath);
|
|
698
|
-
planExists = true;
|
|
699
|
-
} catch {
|
|
700
|
-
planExists = false;
|
|
701
|
-
}
|
|
702
|
-
planModeOptions = { planFilePath, planExists };
|
|
703
|
-
}
|
|
863
|
+
// Inject plan mode system-reminder messages (not system prompt)
|
|
864
|
+
// This preserves prompt caching by keeping the system prompt constant
|
|
865
|
+
const planModeMessages = this.buildPlanModeMessages(currentMode);
|
|
866
|
+
if (planModeMessages.length > 0) {
|
|
867
|
+
recentMessages.push(...planModeMessages);
|
|
704
868
|
}
|
|
705
869
|
|
|
706
870
|
let autoMemoryOptions: { directory: string; content: string } | undefined;
|
|
@@ -733,7 +897,6 @@ export class AIManager {
|
|
|
733
897
|
memory: combinedMemory,
|
|
734
898
|
language: this.getLanguage(),
|
|
735
899
|
isSubagent: !!this.subagentType,
|
|
736
|
-
planMode: planModeOptions,
|
|
737
900
|
autoMemory: autoMemoryOptions,
|
|
738
901
|
permissionMode: currentMode,
|
|
739
902
|
},
|