wave-agent-sdk 0.16.12 → 0.17.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.
Files changed (98) hide show
  1. package/builtin/skills/settings/MCP.md +49 -4
  2. package/builtin/skills/settings/PERMISSIONS.md +31 -0
  3. package/dist/managers/aiManager.d.ts +19 -0
  4. package/dist/managers/aiManager.d.ts.map +1 -1
  5. package/dist/managers/aiManager.js +338 -210
  6. package/dist/managers/backgroundTaskManager.js +1 -1
  7. package/dist/managers/bangManager.js +1 -1
  8. package/dist/managers/hookManager.d.ts +22 -0
  9. package/dist/managers/hookManager.d.ts.map +1 -1
  10. package/dist/managers/hookManager.js +97 -18
  11. package/dist/managers/mcpManager.d.ts.map +1 -1
  12. package/dist/managers/mcpManager.js +53 -41
  13. package/dist/managers/messageManager.d.ts +4 -0
  14. package/dist/managers/messageManager.d.ts.map +1 -1
  15. package/dist/managers/messageManager.js +9 -0
  16. package/dist/managers/permissionManager.d.ts +6 -0
  17. package/dist/managers/permissionManager.d.ts.map +1 -1
  18. package/dist/managers/permissionManager.js +14 -0
  19. package/dist/managers/planManager.d.ts.map +1 -1
  20. package/dist/managers/planManager.js +10 -0
  21. package/dist/managers/pluginManager.d.ts.map +1 -1
  22. package/dist/managers/pluginManager.js +28 -3
  23. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  24. package/dist/managers/slashCommandManager.js +14 -0
  25. package/dist/managers/subagentManager.d.ts.map +1 -1
  26. package/dist/managers/subagentManager.js +4 -0
  27. package/dist/prompts/index.d.ts +0 -4
  28. package/dist/prompts/index.d.ts.map +1 -1
  29. package/dist/prompts/index.js +0 -3
  30. package/dist/prompts/planModeReminders.d.ts +6 -0
  31. package/dist/prompts/planModeReminders.d.ts.map +1 -0
  32. package/dist/prompts/planModeReminders.js +112 -0
  33. package/dist/services/aiService.d.ts +1 -0
  34. package/dist/services/aiService.d.ts.map +1 -1
  35. package/dist/services/aiService.js +3 -1
  36. package/dist/services/configurationService.d.ts.map +1 -1
  37. package/dist/services/configurationService.js +5 -3
  38. package/dist/services/initializationService.d.ts.map +1 -1
  39. package/dist/services/initializationService.js +13 -12
  40. package/dist/services/jsonlHandler.d.ts +1 -1
  41. package/dist/services/jsonlHandler.d.ts.map +1 -1
  42. package/dist/services/jsonlHandler.js +22 -7
  43. package/dist/services/session.d.ts +3 -2
  44. package/dist/services/session.d.ts.map +1 -1
  45. package/dist/services/session.js +30 -13
  46. package/dist/tools/agentTool.js +1 -1
  47. package/dist/tools/bashTool.d.ts.map +1 -1
  48. package/dist/tools/bashTool.js +8 -12
  49. package/dist/tools/editTool.d.ts.map +1 -1
  50. package/dist/tools/editTool.js +21 -8
  51. package/dist/tools/exitPlanMode.d.ts.map +1 -1
  52. package/dist/tools/exitPlanMode.js +2 -0
  53. package/dist/tools/readTool.d.ts.map +1 -1
  54. package/dist/tools/readTool.js +19 -4
  55. package/dist/tools/types.d.ts +2 -0
  56. package/dist/tools/types.d.ts.map +1 -1
  57. package/dist/types/agent.d.ts +4 -0
  58. package/dist/types/agent.d.ts.map +1 -1
  59. package/dist/types/hooks.d.ts +5 -1
  60. package/dist/types/hooks.d.ts.map +1 -1
  61. package/dist/types/hooks.js +2 -0
  62. package/dist/types/mcp.d.ts +1 -0
  63. package/dist/types/mcp.d.ts.map +1 -1
  64. package/dist/utils/containerSetup.d.ts.map +1 -1
  65. package/dist/utils/containerSetup.js +6 -1
  66. package/dist/utils/editUtils.d.ts +3 -2
  67. package/dist/utils/editUtils.d.ts.map +1 -1
  68. package/dist/utils/editUtils.js +5 -3
  69. package/package.json +2 -2
  70. package/src/managers/aiManager.ts +420 -256
  71. package/src/managers/backgroundTaskManager.ts +1 -1
  72. package/src/managers/bangManager.ts +1 -1
  73. package/src/managers/hookManager.ts +125 -21
  74. package/src/managers/mcpManager.ts +65 -49
  75. package/src/managers/messageManager.ts +10 -0
  76. package/src/managers/permissionManager.ts +18 -0
  77. package/src/managers/planManager.ts +11 -0
  78. package/src/managers/pluginManager.ts +52 -6
  79. package/src/managers/slashCommandManager.ts +17 -0
  80. package/src/managers/subagentManager.ts +4 -0
  81. package/src/prompts/index.ts +0 -8
  82. package/src/prompts/planModeReminders.ts +138 -0
  83. package/src/services/aiService.ts +4 -1
  84. package/src/services/configurationService.ts +5 -3
  85. package/src/services/initializationService.ts +16 -15
  86. package/src/services/jsonlHandler.ts +27 -7
  87. package/src/services/session.ts +33 -13
  88. package/src/tools/agentTool.ts +1 -1
  89. package/src/tools/bashTool.ts +8 -11
  90. package/src/tools/editTool.ts +25 -8
  91. package/src/tools/exitPlanMode.ts +3 -0
  92. package/src/tools/readTool.ts +23 -5
  93. package/src/tools/types.ts +2 -0
  94. package/src/types/agent.ts +4 -0
  95. package/src/types/hooks.ts +9 -1
  96. package/src/types/mcp.ts +1 -0
  97. package/src/utils/containerSetup.ts +7 -3
  98. package/src/utils/editUtils.ts +6 -3
@@ -4,7 +4,9 @@ import { microcompactMessages } from "../utils/microcompact.js";
4
4
  import { parseTaskNotificationXml } from "../utils/notificationXml.js";
5
5
  import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
6
6
  import * as fs from "node:fs/promises";
7
+ import { existsSync } from "node:fs";
7
8
  import { buildSystemPrompt } from "../prompts/index.js";
9
+ import { buildPlanModeReminder, buildPlanModeSparseReminder, buildPlanModeReEntryReminder, buildExitedPlanModeReminder, } from "../prompts/planModeReminders.js";
8
10
  import { recoverTruncatedJson } from "../utils/stringUtils.js";
9
11
  import { logger } from "../utils/globalLogger.js";
10
12
  import { startInteractionSpan, endInteractionSpan, startLLMRequestSpan, endLLMRequestSpan, } from "../telemetry/sessionTracing.js";
@@ -116,6 +118,97 @@ export class AIManager {
116
118
  isSubagent: !!this.subagentType,
117
119
  });
118
120
  }
121
+ /**
122
+ * Build plan mode system-reminder messages to inject into the API message stream.
123
+ * These are transient messages not stored in the message history.
124
+ * This preserves prompt caching by keeping the system prompt constant.
125
+ */
126
+ buildPlanModeMessages(currentMode) {
127
+ const messages = [];
128
+ if (!this.permissionManager)
129
+ return messages;
130
+ // Handle exit notification (one-time after leaving plan mode)
131
+ if (this.permissionManager.getNeedsPlanModeExitAttachment()) {
132
+ const planFilePath = this.permissionManager.getPlanFilePath();
133
+ const planExists = planFilePath ? existsSync(planFilePath) : false;
134
+ messages.push({
135
+ role: "user",
136
+ content: buildExitedPlanModeReminder(planFilePath, planExists),
137
+ });
138
+ this.permissionManager.setNeedsPlanModeExitAttachment(false);
139
+ }
140
+ // Handle plan mode reminders
141
+ if (currentMode !== "plan")
142
+ return messages;
143
+ const planFilePath = this.permissionManager.getPlanFilePath();
144
+ if (!planFilePath)
145
+ return messages;
146
+ const planExists = existsSync(planFilePath);
147
+ // Check for re-entry: flag is set AND plan file exists
148
+ if (this.permissionManager.hasExitedPlanModeInSession() && planExists) {
149
+ messages.push({
150
+ role: "user",
151
+ content: buildPlanModeReEntryReminder(planFilePath),
152
+ });
153
+ this.permissionManager.setHasExitedPlanMode(false); // One-time
154
+ }
155
+ // Count plan_mode system-reminders in existing messages to determine full vs sparse
156
+ // and count human turns since last reminder for throttling
157
+ const recentApiMessages = this.messageManager.getMessages();
158
+ let planModeReminderCount = 0;
159
+ let humanTurnsSinceLastReminder = 0;
160
+ let foundLastReminder = false;
161
+ for (let i = recentApiMessages.length - 1; i >= 0; i--) {
162
+ const msg = recentApiMessages[i];
163
+ if (msg.role === "user" && !msg.isMeta) {
164
+ // Count human turns (non-meta user messages without tool results)
165
+ const hasToolResult = msg.blocks?.some((b) => b.type === "tool");
166
+ if (!hasToolResult) {
167
+ if (!foundLastReminder) {
168
+ humanTurnsSinceLastReminder++;
169
+ }
170
+ }
171
+ }
172
+ // Check for existing plan mode system-reminders
173
+ if (msg.role === "user" && msg.isMeta) {
174
+ const textContent = msg.blocks
175
+ ?.filter((b) => b.type === "text")
176
+ .map((b) => ("content" in b ? b.content : ""))
177
+ .join("");
178
+ if (textContent?.includes("Plan mode is active") ||
179
+ textContent?.includes("Plan mode still active")) {
180
+ planModeReminderCount++;
181
+ if (!foundLastReminder) {
182
+ foundLastReminder = true;
183
+ }
184
+ }
185
+ }
186
+ }
187
+ // Throttle: only inject every 5 human turns (but always inject on first turn)
188
+ const TURNS_BETWEEN_REMINDERS = 5;
189
+ const FULL_REMINDER_EVERY_N = 5;
190
+ if (foundLastReminder &&
191
+ humanTurnsSinceLastReminder < TURNS_BETWEEN_REMINDERS) {
192
+ return messages; // Throttled — skip reminder
193
+ }
194
+ // Determine full vs sparse
195
+ // Every 5th reminder is full; rest are sparse
196
+ const reminderNumber = planModeReminderCount + 1;
197
+ const isFull = reminderNumber % FULL_REMINDER_EVERY_N === 1;
198
+ if (isFull) {
199
+ messages.push({
200
+ role: "user",
201
+ content: buildPlanModeReminder(planFilePath, planExists, !!this.subagentType),
202
+ });
203
+ }
204
+ else {
205
+ messages.push({
206
+ role: "user",
207
+ content: buildPlanModeSparseReminder(planFilePath),
208
+ });
209
+ }
210
+ return messages;
211
+ }
119
212
  setIsLoading(isLoading) {
120
213
  this.isLoading = isLoading;
121
214
  this.onLoadingChange?.(isLoading);
@@ -176,211 +269,254 @@ export class AIManager {
176
269
  (usage.cache_creation_input_tokens || 0) >
177
270
  this.getMaxInputTokens()) {
178
271
  logger?.debug(`Token usage exceeded ${this.getMaxInputTokens()}, compacting messages...`);
179
- // Check if messages need compaction
180
272
  const messagesToCompact = this.messageManager.getMessages();
181
- // If there are messages to compact, perform compaction
182
- if (messagesToCompact.length > 0) {
183
- // Circuit breaker: skip compaction after 3 consecutive failures
184
- if (this.consecutiveCompactionFailures >= 3) {
185
- logger?.warn(`Skipping compaction: ${this.consecutiveCompactionFailures} consecutive failures`);
186
- return;
187
- }
188
- const recentChatMessages = convertMessagesForAPI(messagesToCompact);
189
- // Save session before compaction to preserve original messages
190
- await this.messageManager.saveSession();
191
- this.setIsCompacting(true);
273
+ if (messagesToCompact.length === 0)
274
+ return;
275
+ // Circuit breaker: skip compaction after 3 consecutive failures
276
+ if (this.consecutiveCompactionFailures >= 3) {
277
+ logger?.warn(`Skipping compaction: ${this.consecutiveCompactionFailures} consecutive failures`);
278
+ return;
279
+ }
280
+ await this.compactConversation({
281
+ abortSignal: abortController.signal,
282
+ });
283
+ }
284
+ }
285
+ /**
286
+ * Manually compact the conversation history.
287
+ * Called by /compact slash command or auto-compaction trigger.
288
+ */
289
+ async compactConversation(options = {}) {
290
+ const messagesToCompact = this.messageManager.getMessages();
291
+ if (messagesToCompact.length === 0) {
292
+ logger?.debug("No messages to compact");
293
+ return;
294
+ }
295
+ // Circuit breaker: skip if already compacting
296
+ if (this.isCompacting) {
297
+ logger?.warn("Compaction already in progress");
298
+ return;
299
+ }
300
+ // 1. Run PreCompact hooks
301
+ let hookInstructions;
302
+ if (this.hookManager) {
303
+ try {
304
+ const preResult = await this.hookManager.executePreCompactHooks(this.messageManager.getSessionId(), this.messageManager.getTranscriptPath(), options.customInstructions);
305
+ hookInstructions = preResult.additionalInstructions;
306
+ }
307
+ catch (error) {
308
+ logger?.warn(`PreCompact hooks failed: ${error.message}`);
309
+ }
310
+ }
311
+ // 2. Merge custom instructions
312
+ const mergedInstructions = [options.customInstructions, hookInstructions]
313
+ .filter(Boolean)
314
+ .join("\n") || undefined;
315
+ // 3. Save session before compaction
316
+ await this.messageManager.saveSession();
317
+ this.setIsCompacting(true);
318
+ try {
319
+ const recentChatMessages = convertMessagesForAPI(messagesToCompact);
320
+ // 4. Call compactMessages with optional custom instructions
321
+ const compactResult = await aiService.compactMessages({
322
+ gatewayConfig: this.getGatewayConfig(),
323
+ modelConfig: this.getModelConfig(),
324
+ messages: recentChatMessages,
325
+ abortSignal: options.abortSignal,
326
+ model: this.getModelConfig().fastModel,
327
+ customInstructions: mergedInstructions,
328
+ });
329
+ // 5. Handle usage tracking
330
+ let compactUsage;
331
+ if (compactResult.usage) {
332
+ compactUsage = {
333
+ prompt_tokens: compactResult.usage.prompt_tokens,
334
+ completion_tokens: compactResult.usage.completion_tokens,
335
+ total_tokens: compactResult.usage.total_tokens,
336
+ model: this.getModelConfig().fastModel,
337
+ operation_type: "compact",
338
+ };
339
+ }
340
+ // 6. Build post-compact context restoration
341
+ const enhancedSummary = await this.buildPostCompactContext(compactResult.content);
342
+ // 7. Execute message reconstruction
343
+ this.messageManager.compactMessagesAndUpdateSession(enhancedSummary, compactUsage);
344
+ // 8. Track usage
345
+ if (compactUsage && this.callbacks?.onUsageAdded) {
346
+ this.callbacks.onUsageAdded(compactUsage);
347
+ }
348
+ this.consecutiveCompactionFailures = 0;
349
+ // 9. Log OTEL event
350
+ logOTelEvent("compaction", {
351
+ beforeTokens: String(messagesToCompact.length),
352
+ afterTokens: "1",
353
+ model: this.getModelConfig().fastModel,
354
+ }).catch(() => { });
355
+ // 10. Run SessionStart hooks (existing behavior)
356
+ if (this.hookManager) {
192
357
  try {
193
- const compactResult = await aiService.compactMessages({
194
- gatewayConfig: this.getGatewayConfig(),
195
- modelConfig: this.getModelConfig(),
196
- messages: recentChatMessages,
197
- abortSignal: abortController.signal,
198
- model: this.getModelConfig().fastModel,
199
- });
200
- // Handle usage tracking for compaction operations
201
- let compactUsage;
202
- if (compactResult.usage) {
203
- compactUsage = {
204
- prompt_tokens: compactResult.usage.prompt_tokens,
205
- completion_tokens: compactResult.usage.completion_tokens,
206
- total_tokens: compactResult.usage.total_tokens,
207
- model: this.getModelConfig().fastModel,
208
- operation_type: "compact",
209
- };
210
- }
211
- // Build post-compact context restoration
212
- const POST_COMPACT_TOKEN_BUDGET = 50000;
213
- const POST_COMPACT_MAX_TOKENS_PER_FILE = 5000;
214
- const POST_COMPACT_MAX_FILES_TO_RESTORE = 5;
215
- const contextParts = [];
216
- // 1. File context restoration
217
- const recentFiles = this.messageManager.getRecentFileReads(POST_COMPACT_MAX_FILES_TO_RESTORE, POST_COMPACT_MAX_TOKENS_PER_FILE);
218
- let usedTokens = 0;
219
- for (const file of recentFiles) {
220
- const fileTokens = Math.ceil(file.content.length / 4);
221
- if (usedTokens + fileTokens > POST_COMPACT_MAX_TOKENS_PER_FILE)
222
- continue;
223
- if (fileTokens > 0)
224
- usedTokens += fileTokens;
225
- contextParts.push(`\n\n## ${file.path}\n\`\`\`\n${file.content}\n\`\`\``);
226
- if (contextParts.length >= POST_COMPACT_MAX_FILES_TO_RESTORE)
227
- break;
228
- if (usedTokens >= POST_COMPACT_TOKEN_BUDGET)
229
- break;
358
+ const newSessionId = this.messageManager.getSessionId();
359
+ const sessionStartResult = await this.hookManager.executeSessionStartHooks("compact", newSessionId, this.messageManager.getTranscriptPath(), this.subagentType);
360
+ if (sessionStartResult.additionalContext) {
361
+ this.messageManager.addUserMessage({
362
+ content: `<system-reminder>\nSessionStart hook additional context: ${sessionStartResult.additionalContext}\n</system-reminder>`,
363
+ isMeta: true,
364
+ });
230
365
  }
231
- // 2. Working directory
232
- contextParts.push(`\n\n[Working Directory]\nCurrent working directory: ${this.getWorkdir()}`);
233
- // 3. Plan mode context
234
- const currentMode = this.permissionManager?.getCurrentEffectiveMode(this.getModelConfig().permissionMode);
235
- if (currentMode === "plan") {
236
- const planFilePath = this.permissionManager?.getPlanFilePath();
237
- if (planFilePath) {
238
- let planExists = false;
239
- try {
240
- await fs.access(planFilePath);
241
- planExists = true;
242
- }
243
- catch {
244
- // Plan file doesn't exist yet
245
- }
246
- contextParts.push(`\n\n[Plan Mode]\nYou are in plan mode. Plan file: ${planFilePath} (exists: ${planExists})`);
247
- }
366
+ if (sessionStartResult.initialUserMessage) {
367
+ this.messageManager.addUserMessage({
368
+ content: sessionStartResult.initialUserMessage,
369
+ isMeta: true,
370
+ });
248
371
  }
249
- // 4. Invoked skills context (with token budget, matching Claude Code)
250
- const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25000;
251
- const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5000;
252
- const invokedSkillNames = this.messageManager.getInvokedSkillNames(10);
253
- if (invokedSkillNames.length > 0 && this.skillManager) {
254
- const invokedSkillParts = [];
255
- let skillsUsedTokens = 0;
256
- for (const skillName of invokedSkillNames) {
257
- try {
258
- const skill = await this.skillManager.loadSkill(skillName);
259
- if (!skill)
260
- continue;
261
- // Extract content after frontmatter (matching prepareSkillContent pattern)
262
- const contentMatch = skill.content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
263
- let skillContent = contentMatch
264
- ? contentMatch[1].trim()
265
- : skill.content;
266
- // Per-skill token budget enforcement (~4 chars per token)
267
- const maxSkillChars = POST_COMPACT_MAX_TOKENS_PER_SKILL * 4;
268
- if (skillContent.length > maxSkillChars) {
269
- skillContent =
270
- skillContent.slice(0, maxSkillChars) +
271
- "\n\n...[truncated]...";
272
- }
273
- const skillTokens = Math.ceil(skillContent.length / 4);
274
- if (skillsUsedTokens + skillTokens >
275
- POST_COMPACT_SKILLS_TOKEN_BUDGET)
276
- break;
277
- skillsUsedTokens += skillTokens;
278
- invokedSkillParts.push(`\n\n## ${skill.name}\n${skill.description ? `*${skill.description}*\n\n` : ""}\`\`\`\n${skillContent}\n\`\`\``);
279
- }
280
- catch {
281
- // Skip skills that can't be loaded
282
- }
283
- }
284
- if (invokedSkillParts.length > 0) {
285
- contextParts.push(`\n\n[Invoked Skills]\n${invokedSkillParts.join("")}`);
286
- }
372
+ }
373
+ catch (error) {
374
+ logger?.warn(`SessionStart hooks on compact failed: ${error.message}`);
375
+ }
376
+ }
377
+ // 11. Run PostCompact hooks
378
+ if (this.hookManager) {
379
+ try {
380
+ await this.hookManager.executePostCompactHooks(this.messageManager.getSessionId(), this.messageManager.getTranscriptPath(), compactResult.content);
381
+ }
382
+ catch (error) {
383
+ logger?.warn(`PostCompact hooks failed: ${error.message}`);
384
+ }
385
+ }
386
+ logger?.debug(`Successfully compacted ${messagesToCompact.length} messages`);
387
+ }
388
+ catch (compactError) {
389
+ this.consecutiveCompactionFailures++;
390
+ logger?.error(`Failed to compact messages (${this.consecutiveCompactionFailures} consecutive):`, compactError);
391
+ this.messageManager.addErrorBlock(`Failed to compact conversation history: ${compactError instanceof Error ? compactError.message : String(compactError)}. You may encounter context limit issues.`);
392
+ }
393
+ finally {
394
+ this.setIsCompacting(false);
395
+ }
396
+ }
397
+ /**
398
+ * Build post-compact context restoration content.
399
+ * Restores file reads, working directory, plan mode, skills, and background tasks.
400
+ */
401
+ async buildPostCompactContext(summary) {
402
+ const POST_COMPACT_TOKEN_BUDGET = 50000;
403
+ const POST_COMPACT_MAX_TOKENS_PER_FILE = 5000;
404
+ const POST_COMPACT_MAX_FILES_TO_RESTORE = 5;
405
+ const contextParts = [];
406
+ // 1. File context restoration
407
+ const recentFiles = this.messageManager.getRecentFileReads(POST_COMPACT_MAX_FILES_TO_RESTORE, POST_COMPACT_MAX_TOKENS_PER_FILE);
408
+ let usedTokens = 0;
409
+ for (const file of recentFiles) {
410
+ const fileTokens = Math.ceil(file.content.length / 4);
411
+ if (usedTokens + fileTokens > POST_COMPACT_MAX_TOKENS_PER_FILE)
412
+ continue;
413
+ if (fileTokens > 0)
414
+ usedTokens += fileTokens;
415
+ contextParts.push(`\n\n## ${file.path}\n\`\`\`\n${file.content}\n\`\`\``);
416
+ if (contextParts.length >= POST_COMPACT_MAX_FILES_TO_RESTORE)
417
+ break;
418
+ if (usedTokens >= POST_COMPACT_TOKEN_BUDGET)
419
+ break;
420
+ }
421
+ // 2. Working directory
422
+ contextParts.push(`\n\n[Working Directory]\nCurrent working directory: ${this.getWorkdir()}`);
423
+ // 3. Plan mode context
424
+ const currentMode = this.permissionManager?.getCurrentEffectiveMode(this.getModelConfig().permissionMode);
425
+ if (currentMode === "plan") {
426
+ const planFilePath = this.permissionManager?.getPlanFilePath();
427
+ if (planFilePath) {
428
+ let planExists = false;
429
+ try {
430
+ await fs.access(planFilePath);
431
+ planExists = true;
432
+ }
433
+ catch {
434
+ // Plan file doesn't exist yet
435
+ }
436
+ contextParts.push(`\n\n${buildPlanModeReminder(planFilePath, planExists, !!this.subagentType)}`);
437
+ }
438
+ }
439
+ // 4. Invoked skills context (with token budget, matching Claude Code)
440
+ const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25000;
441
+ const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5000;
442
+ const invokedSkillNames = this.messageManager.getInvokedSkillNames(10);
443
+ if (invokedSkillNames.length > 0 && this.skillManager) {
444
+ const invokedSkillParts = [];
445
+ let skillsUsedTokens = 0;
446
+ for (const skillName of invokedSkillNames) {
447
+ try {
448
+ const skill = await this.skillManager.loadSkill(skillName);
449
+ if (!skill)
450
+ continue;
451
+ const contentMatch = skill.content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
452
+ let skillContent = contentMatch
453
+ ? contentMatch[1].trim()
454
+ : skill.content;
455
+ const maxSkillChars = POST_COMPACT_MAX_TOKENS_PER_SKILL * 4;
456
+ if (skillContent.length > maxSkillChars) {
457
+ skillContent =
458
+ skillContent.slice(0, maxSkillChars) + "\n\n...[truncated]...";
287
459
  }
288
- // 5. Background subagent status (shell tasks excluded, matching Claude Code's createAsyncAgentAttachmentsIfNeeded)
289
- const agents = this.backgroundTaskManager
290
- ?.getAllTasks()
291
- .filter((a) => a.type === "subagent") || [];
292
- if (agents.length > 0) {
293
- const agentParts = [];
294
- for (const a of agents) {
295
- if (a.status === "killed") {
296
- agentParts.push(`Task "${a.description}" (${a.id}) was stopped by the user.`);
297
- }
298
- else if (a.status === "running") {
299
- const parts = [
300
- `Background agent "${a.description}" (${a.id}) is still running.`,
301
- `Do NOT spawn a duplicate. You will be notified when it completes.`,
302
- ];
303
- if (a.outputPath) {
304
- parts.push(`You can read partial output at ${a.outputPath}.`);
305
- }
306
- agentParts.push(parts.join(" "));
307
- }
308
- else {
309
- // completed or failed
310
- const parts = [
311
- `Task ${a.id} (status: ${a.status}) (description: ${a.description}).`,
312
- ];
313
- const deltaText = a.status === "failed" ? a.stderr : a.stdout;
314
- if (deltaText && deltaText.length > 0) {
315
- const summary = deltaText.length > 500
316
- ? deltaText.slice(0, 500) + "..."
317
- : deltaText;
318
- parts.push(`Delta: ${summary}`);
319
- }
320
- if (a.outputPath) {
321
- parts.push(`Read the output file to retrieve the result: ${a.outputPath}.`);
322
- }
323
- agentParts.push(parts.join(" "));
324
- }
325
- }
326
- if (agentParts.length > 0) {
327
- contextParts.push(`\n\n[Background Tasks]\n${agentParts.join("\n")}`);
328
- }
460
+ const skillTokens = Math.ceil(skillContent.length / 4);
461
+ if (skillsUsedTokens + skillTokens > POST_COMPACT_SKILLS_TOKEN_BUDGET)
462
+ break;
463
+ skillsUsedTokens += skillTokens;
464
+ invokedSkillParts.push(`\n\n## ${skill.name}\n${skill.description ? `*${skill.description}*\n\n` : ""}\`\`\`\n${skillContent}\n\`\`\``);
465
+ }
466
+ catch {
467
+ // Skip skills that can't be loaded
468
+ }
469
+ }
470
+ if (invokedSkillParts.length > 0) {
471
+ contextParts.push(`\n\n[Invoked Skills]\n${invokedSkillParts.join("")}`);
472
+ }
473
+ }
474
+ // 5. Background subagent status (shell tasks excluded, matching Claude Code's createAsyncAgentAttachmentsIfNeeded)
475
+ const agents = this.backgroundTaskManager
476
+ ?.getAllTasks()
477
+ .filter((a) => a.type === "subagent") || [];
478
+ if (agents.length > 0) {
479
+ const agentParts = [];
480
+ for (const a of agents) {
481
+ if (a.status === "killed") {
482
+ agentParts.push(`Task "${a.description}" (${a.id}) was stopped by the user.`);
483
+ }
484
+ else if (a.status === "running") {
485
+ const parts = [
486
+ `Background agent "${a.description}" (${a.id}) is still running.`,
487
+ `Do NOT spawn a duplicate. You will be notified when it completes.`,
488
+ ];
489
+ if (a.outputPath) {
490
+ parts.push(`You can read partial output at ${a.outputPath}.`);
329
491
  }
330
- // Merge context restoration into summary
331
- const enhancedSummary = compactResult.content +
332
- (contextParts.length > 0
333
- ? `\n\n[Context Restoration]` + contextParts.join("")
334
- : "");
335
- // Execute message reconstruction and sessionId update after compaction
336
- this.messageManager.compactMessagesAndUpdateSession(enhancedSummary, compactUsage);
337
- // Notify Agent to add to usage tracking
338
- if (compactUsage && this.callbacks?.onUsageAdded) {
339
- this.callbacks.onUsageAdded(compactUsage);
492
+ agentParts.push(parts.join(" "));
493
+ }
494
+ else {
495
+ // completed or failed
496
+ const parts = [
497
+ `Task ${a.id} (status: ${a.status}) (description: ${a.description}).`,
498
+ ];
499
+ const deltaText = a.status === "failed" ? a.stderr : a.stdout;
500
+ if (deltaText && deltaText.length > 0) {
501
+ const summary = deltaText.length > 500
502
+ ? deltaText.slice(0, 500) + "..."
503
+ : deltaText;
504
+ parts.push(`Delta: ${summary}`);
340
505
  }
341
- logger?.debug(`Successfully compacted ${messagesToCompact.length} messages and updated session`);
342
- this.consecutiveCompactionFailures = 0;
343
- // Log compaction event
344
- logOTelEvent("compaction", {
345
- beforeTokens: String(messagesToCompact.length),
346
- afterTokens: "1",
347
- model: this.getModelConfig().fastModel,
348
- }).catch(() => { });
349
- // Run SessionStart hooks after compaction to restore context
350
- if (this.hookManager) {
351
- try {
352
- const newSessionId = this.messageManager.getSessionId();
353
- const sessionStartResult = await this.hookManager.executeSessionStartHooks("compact", newSessionId, this.messageManager.getTranscriptPath(), this.subagentType);
354
- // Inject additionalContext as a meta user message
355
- if (sessionStartResult.additionalContext) {
356
- this.messageManager.addUserMessage({
357
- content: `<system-reminder>\nSessionStart hook additional context: ${sessionStartResult.additionalContext}\n</system-reminder>`,
358
- isMeta: true,
359
- });
360
- }
361
- // Inject initialUserMessage as a meta user message
362
- if (sessionStartResult.initialUserMessage) {
363
- this.messageManager.addUserMessage({
364
- content: sessionStartResult.initialUserMessage,
365
- isMeta: true,
366
- });
367
- }
368
- }
369
- catch (error) {
370
- logger?.warn(`SessionStart hooks on compact failed: ${error.message}`);
371
- }
506
+ if (a.outputPath) {
507
+ parts.push(`Read the output file to retrieve the result: ${a.outputPath}.`);
372
508
  }
509
+ agentParts.push(parts.join(" "));
373
510
  }
374
- catch (compactError) {
375
- this.consecutiveCompactionFailures++;
376
- logger?.error(`Failed to compact messages (${this.consecutiveCompactionFailures} consecutive):`, compactError);
377
- this.messageManager.addErrorBlock(`Failed to compact conversation history: ${compactError instanceof Error ? compactError.message : String(compactError)}. You may encounter context limit issues.`);
378
- }
379
- finally {
380
- this.setIsCompacting(false);
381
- }
511
+ }
512
+ if (agentParts.length > 0) {
513
+ contextParts.push(`\n\n[Background Tasks]\n${agentParts.join("\n")}`);
382
514
  }
383
515
  }
516
+ return (summary +
517
+ (contextParts.length > 0
518
+ ? `\n\n[Context Restoration]` + contextParts.join("")
519
+ : ""));
384
520
  }
385
521
  getIsCompacting() {
386
522
  return this.isCompacting;
@@ -477,20 +613,11 @@ export class AIManager {
477
613
  const filteredToolPlugins = this.toolManager
478
614
  .getTools()
479
615
  .filter((t) => toolNames.has(t.name));
480
- let planModeOptions;
481
- if (currentMode === "plan") {
482
- const planFilePath = this.permissionManager?.getPlanFilePath();
483
- if (planFilePath) {
484
- let planExists = false;
485
- try {
486
- await fs.access(planFilePath);
487
- planExists = true;
488
- }
489
- catch {
490
- planExists = false;
491
- }
492
- planModeOptions = { planFilePath, planExists };
493
- }
616
+ // Inject plan mode system-reminder messages (not system prompt)
617
+ // This preserves prompt caching by keeping the system prompt constant
618
+ const planModeMessages = this.buildPlanModeMessages(currentMode);
619
+ if (planModeMessages.length > 0) {
620
+ recentMessages.push(...planModeMessages);
494
621
  }
495
622
  let autoMemoryOptions;
496
623
  if (this.getAutoMemoryEnabled()) {
@@ -513,7 +640,6 @@ export class AIManager {
513
640
  memory: combinedMemory,
514
641
  language: this.getLanguage(),
515
642
  isSubagent: !!this.subagentType,
516
- planMode: planModeOptions,
517
643
  autoMemory: autoMemoryOptions,
518
644
  permissionMode: currentMode,
519
645
  }), // Pass custom system prompt
@@ -742,6 +868,7 @@ export class AIManager {
742
868
  backgroundTaskManager: this.backgroundTaskManager,
743
869
  workdir: this.getWorkdir(),
744
870
  originalWorkdir: this.originalWorkdir,
871
+ env: this.container.get("MergedEnv"),
745
872
  messageId: this.messageManager.getMessages().slice(-1)[0]?.id,
746
873
  sessionId: this.messageManager.getSessionId(),
747
874
  toolCallId: toolId,
@@ -1122,7 +1249,8 @@ export class AIManager {
1122
1249
  toolInput,
1123
1250
  toolResponse,
1124
1251
  subagentType: this.subagentType, // Include subagent type in hook context
1125
- env: Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined)), // Include environment variables
1252
+ env: this.container.get("MergedEnv") ||
1253
+ process.env,
1126
1254
  };
1127
1255
  const results = await this.hookManager.executeHooks("PostToolUse", context);
1128
1256
  // Process hook results to handle exit codes and update tool results
@@ -39,7 +39,7 @@ export class BackgroundTaskManager {
39
39
  stdio: "pipe",
40
40
  detached: true,
41
41
  cwd: this.workdir,
42
- env: {
42
+ env: this.container.get("MergedEnv") || {
43
43
  ...process.env,
44
44
  },
45
45
  });
@@ -25,7 +25,7 @@ export class BangManager {
25
25
  shell: true,
26
26
  stdio: "pipe",
27
27
  cwd: this.workdir,
28
- env: {
28
+ env: this.container.get("MergedEnv") || {
29
29
  ...process.env,
30
30
  },
31
31
  });