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.
Files changed (59) hide show
  1. package/dist/agent.d.ts +6 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +16 -2
  4. package/dist/managers/aiManager.d.ts +3 -0
  5. package/dist/managers/aiManager.d.ts.map +1 -1
  6. package/dist/managers/aiManager.js +93 -8
  7. package/dist/managers/messageManager.d.ts +15 -0
  8. package/dist/managers/messageManager.d.ts.map +1 -1
  9. package/dist/managers/messageManager.js +52 -2
  10. package/dist/managers/messageQueue.d.ts +1 -0
  11. package/dist/managers/messageQueue.d.ts.map +1 -1
  12. package/dist/managers/messageQueue.js +8 -0
  13. package/dist/managers/permissionManager.d.ts +4 -0
  14. package/dist/managers/permissionManager.d.ts.map +1 -1
  15. package/dist/managers/permissionManager.js +6 -0
  16. package/dist/managers/subagentManager.d.ts.map +1 -1
  17. package/dist/managers/subagentManager.js +23 -17
  18. package/dist/prompts/index.d.ts +2 -1
  19. package/dist/prompts/index.d.ts.map +1 -1
  20. package/dist/prompts/index.js +50 -25
  21. package/dist/services/aiService.d.ts.map +1 -1
  22. package/dist/services/aiService.js +11 -1
  23. package/dist/tools/agentTool.d.ts.map +1 -1
  24. package/dist/tools/agentTool.js +14 -2
  25. package/dist/tools/bashTool.d.ts.map +1 -1
  26. package/dist/tools/bashTool.js +27 -5
  27. package/dist/tools/types.d.ts +1 -0
  28. package/dist/tools/types.d.ts.map +1 -1
  29. package/dist/tools/webFetchTool.d.ts.map +1 -1
  30. package/dist/tools/webFetchTool.js +202 -78
  31. package/dist/types/messaging.d.ts +1 -0
  32. package/dist/types/messaging.d.ts.map +1 -1
  33. package/dist/utils/convertMessagesForAPI.js +1 -1
  34. package/dist/utils/groupMessagesByApiRound.d.ts +24 -0
  35. package/dist/utils/groupMessagesByApiRound.d.ts.map +1 -0
  36. package/dist/utils/groupMessagesByApiRound.js +97 -0
  37. package/dist/utils/messageOperations.d.ts +1 -0
  38. package/dist/utils/messageOperations.d.ts.map +1 -1
  39. package/dist/utils/microcompact.d.ts +7 -0
  40. package/dist/utils/microcompact.d.ts.map +1 -0
  41. package/dist/utils/microcompact.js +78 -0
  42. package/package.json +2 -1
  43. package/src/agent.ts +17 -2
  44. package/src/managers/aiManager.ts +117 -15
  45. package/src/managers/messageManager.ts +64 -2
  46. package/src/managers/messageQueue.ts +9 -0
  47. package/src/managers/permissionManager.ts +7 -0
  48. package/src/managers/subagentManager.ts +28 -24
  49. package/src/prompts/index.ts +51 -25
  50. package/src/services/aiService.ts +14 -1
  51. package/src/tools/agentTool.ts +14 -2
  52. package/src/tools/bashTool.ts +27 -5
  53. package/src/tools/types.ts +1 -0
  54. package/src/tools/webFetchTool.ts +276 -86
  55. package/src/types/messaging.ts +1 -0
  56. package/src/utils/convertMessagesForAPI.ts +1 -1
  57. package/src/utils/groupMessagesByApiRound.ts +120 -0
  58. package/src/utils/messageOperations.ts +1 -0
  59. 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: 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: model || this.getModelConfig().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
- compressionResult.content,
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
- logger?.error("Failed to compress messages:", compressError);
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 recentMessages = convertMessagesForAPI(
408
- this.messageManager.getMessages(),
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 3 messages to preserve
499
- const lastThreeMessages = this.messages.slice(-3);
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
- // Enqueue completion notification
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
- );
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
- // Enqueue error notification
614
- const notificationQueue = this.container.has("NotificationQueue")
615
- ? this.container.get<NotificationQueue>("NotificationQueue")
616
- : undefined;
617
- if (notificationQueue) {
618
- const errorMsg =
619
- error instanceof Error ? error.message : String(error);
620
- const summary = `Agent task "${instance.description}" failed: ${errorMsg}`;
621
- notificationQueue.enqueue(
622
- `<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>`,
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;
@@ -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 have been working on the task described above but have not yet completed it. Write a 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. Your summary should be structured, concise, and actionable. Include:
183
- 1. Task Overview
184
- The user's core request and success criteria
185
- Any clarifications or constraints they specified
186
- 2. Current State
187
- What has been completed so far
188
- Files created, modified, or analyzed (with paths if relevant)
189
- Key outputs or artifacts produced
190
- 3. Important Discoveries
191
- Technical constraints or requirements uncovered
192
- Decisions made and their rationale
193
- Errors encountered and how they were resolved
194
- What approaches were tried that didn't work (and why)
195
- 4. Next Steps
196
- Specific actions needed to complete the task
197
- Any blockers or open questions to resolve
198
- Priority order if multiple steps remain
199
- 5. Context to Preserve
200
- User preferences or style requirements
201
- Domain-specific details that aren't obvious
202
- Any promises made to the user
203
- Be concise but complete—err on the side of including information that would prevent duplicate work or repeated mistakes. Write in a way that enables immediate resumption of the task.
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
- if (options.workdir) {
255
- const isGitRepo = isGitRepository(options.workdir);
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: ${options.workdir}
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
- ...messages,
837
+ ...cleanedMessages,
825
838
  {
826
839
  role: "user",
827
840
  content: `Please create a detailed summary of the conversation so far.`,
@@ -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: `Agent started in background with ID: ${result}.${outputPath ? ` Real-time output: ${outputPath}` : ""}`,
229
+ content: backgroundMsg,
218
230
  shortResult: `Agent started in background: ${result}`,
219
231
  });
220
232
  return;