wave-agent-sdk 0.12.10 → 0.13.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/constants/tools.d.ts +1 -0
  2. package/dist/constants/tools.d.ts.map +1 -1
  3. package/dist/constants/tools.js +1 -0
  4. package/dist/managers/aiManager.d.ts.map +1 -1
  5. package/dist/managers/aiManager.js +5 -3
  6. package/dist/managers/mcpManager.d.ts +9 -0
  7. package/dist/managers/mcpManager.d.ts.map +1 -1
  8. package/dist/managers/mcpManager.js +46 -1
  9. package/dist/managers/messageManager.d.ts +10 -0
  10. package/dist/managers/messageManager.d.ts.map +1 -1
  11. package/dist/managers/messageManager.js +39 -0
  12. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  13. package/dist/managers/slashCommandManager.js +15 -9
  14. package/dist/managers/subagentManager.d.ts.map +1 -1
  15. package/dist/managers/subagentManager.js +4 -2
  16. package/dist/managers/toolManager.d.ts.map +1 -1
  17. package/dist/managers/toolManager.js +9 -1
  18. package/dist/services/MarketplaceService.d.ts +6 -1
  19. package/dist/services/MarketplaceService.d.ts.map +1 -1
  20. package/dist/services/MarketplaceService.js +34 -1
  21. package/dist/services/configurationService.d.ts +1 -5
  22. package/dist/services/configurationService.d.ts.map +1 -1
  23. package/dist/services/configurationService.js +21 -21
  24. package/dist/services/initializationService.d.ts.map +1 -1
  25. package/dist/services/initializationService.js +1 -1
  26. package/dist/services/interactionService.d.ts.map +1 -1
  27. package/dist/services/interactionService.js +2 -2
  28. package/dist/tools/enterPlanMode.d.ts +6 -0
  29. package/dist/tools/enterPlanMode.d.ts.map +1 -0
  30. package/dist/tools/enterPlanMode.js +87 -0
  31. package/dist/types/messaging.d.ts +4 -1
  32. package/dist/types/messaging.d.ts.map +1 -1
  33. package/dist/types/permissions.d.ts +1 -1
  34. package/dist/types/permissions.d.ts.map +1 -1
  35. package/dist/types/permissions.js +2 -1
  36. package/dist/utils/containerSetup.js +2 -4
  37. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  38. package/dist/utils/convertMessagesForAPI.js +2 -1
  39. package/dist/utils/messageOperations.d.ts +2 -1
  40. package/dist/utils/messageOperations.d.ts.map +1 -1
  41. package/dist/utils/messageOperations.js +14 -6
  42. package/package.json +1 -1
  43. package/src/constants/tools.ts +1 -0
  44. package/src/managers/aiManager.ts +12 -3
  45. package/src/managers/mcpManager.ts +56 -1
  46. package/src/managers/messageManager.ts +43 -0
  47. package/src/managers/slashCommandManager.ts +17 -10
  48. package/src/managers/subagentManager.ts +4 -2
  49. package/src/managers/toolManager.ts +13 -1
  50. package/src/services/MarketplaceService.ts +33 -1
  51. package/src/services/configurationService.ts +21 -22
  52. package/src/services/initializationService.ts +3 -1
  53. package/src/services/interactionService.ts +3 -2
  54. package/src/tools/enterPlanMode.ts +107 -0
  55. package/src/types/messaging.ts +4 -1
  56. package/src/types/permissions.ts +2 -0
  57. package/src/utils/containerSetup.ts +4 -4
  58. package/src/utils/convertMessagesForAPI.ts +2 -1
  59. package/src/utils/messageOperations.ts +15 -5
@@ -430,6 +430,8 @@ export class MessageManager {
430
430
  }
431
431
 
432
432
  public updateToolBlock(params: AgentToolBlockUpdateParams): void {
433
+ // Finalize any streaming text/reasoning blocks before adding/updating a tool block
434
+ this.finalizeCurrentStreamingBlocks();
433
435
  const newMessages = updateToolBlockInMessage({
434
436
  messages: this.messages,
435
437
  ...params,
@@ -447,6 +449,8 @@ export class MessageManager {
447
449
  messageId: string,
448
450
  params: Omit<AgentToolBlockUpdateParams, "id">,
449
451
  ): string {
452
+ // Finalize any streaming text/reasoning blocks before adding a tool block
453
+ this.finalizeCurrentStreamingBlocks();
450
454
  const { messages: newMessages, toolBlockId } =
451
455
  addToolBlockToMessageInMessages(this.messages, messageId, params);
452
456
  this.setMessages(newMessages);
@@ -647,12 +651,14 @@ export class MessageManager {
647
651
  lastMessage.blocks[textBlockIndex] = {
648
652
  type: "text",
649
653
  content: newAccumulatedContent,
654
+ stage: "streaming",
650
655
  };
651
656
  } else {
652
657
  // Add new text block if none exists
653
658
  lastMessage.blocks.push({
654
659
  type: "text",
655
660
  content: newAccumulatedContent,
661
+ stage: "streaming",
656
662
  });
657
663
  }
658
664
 
@@ -696,12 +702,14 @@ export class MessageManager {
696
702
  lastMessage.blocks[reasoningBlockIndex] = {
697
703
  type: "reasoning",
698
704
  content: newAccumulatedReasoning,
705
+ stage: "streaming",
699
706
  };
700
707
  } else {
701
708
  // Add new reasoning block if none exists
702
709
  lastMessage.blocks.push({
703
710
  type: "reasoning",
704
711
  content: newAccumulatedReasoning,
712
+ stage: "streaming",
705
713
  });
706
714
  }
707
715
 
@@ -714,6 +722,41 @@ export class MessageManager {
714
722
  this.callbacks.onMessagesChange?.([...this.messages]); // Still need to notify of changes
715
723
  }
716
724
 
725
+ /**
726
+ * Public wrapper for finalizeCurrentStreamingBlocks.
727
+ * Finalizes text/reasoning blocks after streaming completes (e.g. final response with no tools).
728
+ */
729
+ public finalizeStreamingBlocks(): void {
730
+ this.finalizeCurrentStreamingBlocks();
731
+ }
732
+
733
+ /**
734
+ * Finalize streaming text/reasoning blocks by setting their stage to "end".
735
+ * Called when a new block (e.g. tool) is appended during streaming.
736
+ */
737
+ private finalizeCurrentStreamingBlocks(): void {
738
+ if (this.messages.length === 0) return;
739
+ const lastMessage = this.messages[this.messages.length - 1];
740
+ if (lastMessage.role !== "assistant") return;
741
+
742
+ const newBlocks = lastMessage.blocks.map((block) => {
743
+ if (
744
+ (block.type === "text" || block.type === "reasoning") &&
745
+ block.stage === "streaming"
746
+ ) {
747
+ return { ...block, stage: "end" as const };
748
+ }
749
+ return block;
750
+ });
751
+
752
+ // Only update if something changed
753
+ const changed = newBlocks.some((b, i) => b !== lastMessage.blocks[i]);
754
+ if (changed) {
755
+ lastMessage.blocks = newBlocks;
756
+ this.callbacks.onMessagesChange?.([...this.messages]);
757
+ }
758
+ }
759
+
717
760
  /**
718
761
  * Remove the last user message from the conversation
719
762
  * Used for hook error handling when the user prompt needs to be erased
@@ -179,7 +179,8 @@ export class SlashCommandManager {
179
179
  if (skill.context === "fork") {
180
180
  // Forked skill execution: add user message with text + tool block
181
181
  const messageId = this.messageManager.addUserMessage({
182
- content: `/${skill.name} ${args || ""}`,
182
+ content: `/${skill.name}${args ? ` ${args}` : ""}`,
183
+ customCommandContent: prepared.content,
183
184
  });
184
185
 
185
186
  const toolBlockId = this.messageManager.addToolBlockToMessage(
@@ -285,7 +286,8 @@ export class SlashCommandManager {
285
286
 
286
287
  // Add user message with the processed content
287
288
  this.messageManager.addUserMessage({
288
- content: result.content,
289
+ content: `/${skill.name}${args ? ` ${args}` : ""}`,
290
+ customCommandContent: result.content,
289
291
  });
290
292
 
291
293
  // Trigger AI response
@@ -489,20 +491,25 @@ export class SlashCommandManager {
489
491
  // Parse bash commands from the content
490
492
  const { commands, processedContent } = parseBashCommands(content);
491
493
 
492
- // Execute bash commands if any
493
- let finalContent = processedContent;
494
+ // Add user message immediately so text block shows before bash execution
495
+ const messageId = this.messageManager.addUserMessage({
496
+ content: `/${commandName}`,
497
+ customCommandContent: processedContent,
498
+ });
499
+
500
+ // Execute bash commands and update the message if any exist
494
501
  if (commands.length > 0) {
495
502
  const bashResults = await executeBashCommands(commands, this.workdir);
496
- finalContent = replaceBashCommandsWithOutput(
503
+ const finalContent = replaceBashCommandsWithOutput(
497
504
  processedContent,
498
505
  bashResults,
499
506
  );
500
- }
501
507
 
502
- // Add user message with the processed content
503
- this.messageManager.addUserMessage({
504
- content: finalContent,
505
- });
508
+ // Update the user message with the bash-processed content
509
+ this.messageManager.updateUserMessage(messageId, {
510
+ customCommandContent: finalContent,
511
+ });
512
+ }
506
513
 
507
514
  // Execute the AI conversation with custom configuration
508
515
  await this.aiManager.sendAIMessage({
@@ -343,7 +343,8 @@ export class SubagentManager {
343
343
  outputPath: logPath,
344
344
  subagentId: instance.subagentId,
345
345
  onStop: () => {
346
- instance.logStream?.end();
346
+ instance.logStream?.destroy();
347
+ instance.logStream = undefined;
347
348
  instance.aiManager.abortAIMessage();
348
349
  this.cleanupInstance(instance.subagentId);
349
350
  },
@@ -421,7 +422,8 @@ export class SubagentManager {
421
422
  outputPath: logPath,
422
423
  subagentId: instance.subagentId,
423
424
  onStop: () => {
424
- instance.logStream?.end();
425
+ instance.logStream?.destroy();
426
+ instance.logStream = undefined;
425
427
  instance.aiManager.abortAIMessage();
426
428
  this.cleanupInstance(instance.subagentId);
427
429
  },
@@ -4,6 +4,7 @@ import { taskStopTool } from "../tools/taskStopTool.js";
4
4
  import { editTool } from "../tools/editTool.js";
5
5
  import { writeTool } from "../tools/writeTool.js";
6
6
  import { exitPlanModeTool } from "../tools/exitPlanMode.js";
7
+ import { enterPlanModeTool } from "../tools/enterPlanMode.js";
7
8
  import { askUserQuestionTool } from "../tools/askUserQuestion.js";
8
9
  import { cronCreateTool } from "../tools/cronCreateTool.js";
9
10
  import { cronDeleteTool } from "../tools/cronDeleteTool.js";
@@ -106,6 +107,7 @@ class ToolManager {
106
107
  editTool,
107
108
  writeTool,
108
109
  exitPlanModeTool,
110
+ enterPlanModeTool,
109
111
  askUserQuestionTool,
110
112
  globTool,
111
113
  grepTool,
@@ -296,13 +298,23 @@ class ToolManager {
296
298
  return false;
297
299
  }
298
300
  if (effectivePermissionMode === "bypassPermissions") {
299
- if (tool.name === "ExitPlanMode" || tool.name === "AskUserQuestion") {
301
+ if (
302
+ tool.name === "ExitPlanMode" ||
303
+ tool.name === "AskUserQuestion" ||
304
+ tool.name === "EnterPlanMode"
305
+ ) {
300
306
  return false;
301
307
  }
302
308
  }
303
309
  if (tool.name === "ExitPlanMode") {
304
310
  return effectivePermissionMode === "plan";
305
311
  }
312
+ if (tool.name === "EnterPlanMode") {
313
+ return (
314
+ effectivePermissionMode !== "plan" &&
315
+ effectivePermissionMode !== "bypassPermissions"
316
+ );
317
+ }
306
318
  return true;
307
319
  })
308
320
  .map((tool) => {
@@ -69,7 +69,28 @@ export class MarketplaceService {
69
69
  }
70
70
 
71
71
  /**
72
- * Acquires a file-based lock and executes the provided function.
72
+ * Check if a lock file is stale by reading its PID and checking if the process is alive.
73
+ * Returns true if the lock is stale and safe to remove.
74
+ */
75
+ private async isStaleLock(): Promise<boolean> {
76
+ try {
77
+ const content = await fs.readFile(this.lockPath, "utf-8");
78
+ const pid = parseInt(content.trim(), 10);
79
+ if (isNaN(pid)) return true;
80
+ // Check if the process is still running
81
+ try {
82
+ process.kill(pid, 0);
83
+ return false; // Process exists, lock is valid
84
+ } catch {
85
+ return true; // Process doesn't exist, lock is stale
86
+ }
87
+ } catch {
88
+ return true; // Can't read lock file, assume stale
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Acquires a file-based lock (with PID tracking for stale lock detection) and executes the provided function.
73
94
  * Supports re-entrancy within the same process.
74
95
  */
75
96
  private async withLock<T>(fn: () => Promise<T>): Promise<T> {
@@ -92,6 +113,14 @@ export class MarketplaceService {
92
113
  "code" in error &&
93
114
  error.code === "EEXIST"
94
115
  ) {
116
+ // Check for stale lock every 60 retries (every ~6 seconds)
117
+ if (i > 0 && i % 60 === 0) {
118
+ const stale = await this.isStaleLock();
119
+ if (stale) {
120
+ await fs.unlink(this.lockPath).catch(() => {});
121
+ continue;
122
+ }
123
+ }
95
124
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
96
125
  continue;
97
126
  }
@@ -105,6 +134,9 @@ export class MarketplaceService {
105
134
  );
106
135
  }
107
136
 
137
+ // Write PID into the lock file for stale lock detection
138
+ await fs.writeFile(this.lockPath, String(process.pid), "utf-8");
139
+
108
140
  MarketplaceService.isLockedInProcess = true;
109
141
  try {
110
142
  return await fn();
@@ -8,6 +8,7 @@
8
8
  import { readFileSync, existsSync, promises as fs } from "fs";
9
9
  import * as path from "path";
10
10
  import { isValidHookEvent } from "../types/hooks.js";
11
+ import { logger } from "../utils/globalLogger.js";
11
12
  import type {
12
13
  ConfigurationLoadResult,
13
14
  ValidationResult,
@@ -50,8 +51,8 @@ import { parseCustomHeaders } from "../utils/stringUtils.js";
50
51
  */
51
52
  export class ConfigurationService {
52
53
  private currentConfiguration: WaveConfiguration | null = null;
53
- private env: Record<string, string> = {};
54
54
  private options: AgentOptions = {};
55
+ private _configuredEnvKeys = new Set<string>();
55
56
 
56
57
  /**
57
58
  * Set agent options for configuration resolution
@@ -356,14 +357,13 @@ export class ConfigurationService {
356
357
  * This replaces direct process.env modification
357
358
  */
358
359
  setEnvironmentVars(env: Record<string, string>): void {
359
- this.env = { ...env };
360
- }
361
-
362
- /**
363
- * Get current environment variables
364
- */
365
- getEnvironmentVars(): Record<string, string> {
366
- return { ...this.env };
360
+ for (const [key, value] of Object.entries(env)) {
361
+ if (process.env[key] !== undefined && !this._configuredEnvKeys.has(key)) {
362
+ logger.warn(`Overriding environment variable: ${key}`);
363
+ }
364
+ process.env[key] = value;
365
+ this._configuredEnvKeys.add(key);
366
+ }
367
367
  }
368
368
 
369
369
  // =============================================================================
@@ -396,7 +396,7 @@ export class ConfigurationService {
396
396
  } else if (this.options.apiKey !== undefined) {
397
397
  resolvedApiKey = this.options.apiKey;
398
398
  } else {
399
- resolvedApiKey = this.env.WAVE_API_KEY;
399
+ resolvedApiKey = process.env.WAVE_API_KEY;
400
400
  }
401
401
 
402
402
  // Resolve base URL: override > options > env (settings.json) > process.env
@@ -407,7 +407,7 @@ export class ConfigurationService {
407
407
  } else if (this.options.baseURL !== undefined) {
408
408
  resolvedBaseURL = this.options.baseURL;
409
409
  } else {
410
- resolvedBaseURL = this.env.WAVE_BASE_URL || "";
410
+ resolvedBaseURL = process.env.WAVE_BASE_URL || "";
411
411
  }
412
412
 
413
413
  // Fallback to process.env if still not resolved (for dynamic updates in tests)
@@ -422,7 +422,7 @@ export class ConfigurationService {
422
422
  throw new ConfigurationError(CONFIG_ERRORS.MISSING_BASE_URL, "baseURL", {
423
423
  constructor: baseURL,
424
424
  environment: process.env.WAVE_BASE_URL,
425
- settings: this.env.WAVE_BASE_URL,
425
+ settings: process.env.WAVE_BASE_URL,
426
426
  });
427
427
  }
428
428
 
@@ -436,7 +436,7 @@ export class ConfigurationService {
436
436
 
437
437
  // Resolve custom headers from environment: env (settings.json) > process.env
438
438
  const envCustomHeaders =
439
- this.env.WAVE_CUSTOM_HEADERS || process.env.WAVE_CUSTOM_HEADERS || "";
439
+ process.env.WAVE_CUSTOM_HEADERS || process.env.WAVE_CUSTOM_HEADERS || "";
440
440
  const parsedEnvHeaders = parseCustomHeaders(envCustomHeaders);
441
441
 
442
442
  // Merge headers: env headers < options < override
@@ -475,14 +475,14 @@ export class ConfigurationService {
475
475
  const resolvedAgentModel =
476
476
  model ||
477
477
  this.options.model ||
478
- this.env.WAVE_MODEL ||
478
+ process.env.WAVE_MODEL ||
479
479
  process.env.WAVE_MODEL;
480
480
 
481
481
  // Resolve fast model: override > options > env (settings.json) > process.env
482
482
  const resolvedFastModel =
483
483
  fastModel ||
484
484
  this.options.fastModel ||
485
- this.env.WAVE_FAST_MODEL ||
485
+ process.env.WAVE_FAST_MODEL ||
486
486
  process.env.WAVE_FAST_MODEL;
487
487
 
488
488
  // Validate required fields
@@ -490,7 +490,6 @@ export class ConfigurationService {
490
490
  throw new ConfigurationError(CONFIG_ERRORS.MISSING_MODEL, "model", {
491
491
  constructor: model,
492
492
  environment: process.env.WAVE_MODEL,
493
- settings: this.env.WAVE_MODEL,
494
493
  });
495
494
  }
496
495
 
@@ -501,7 +500,6 @@ export class ConfigurationService {
501
500
  {
502
501
  constructor: fastModel,
503
502
  environment: process.env.WAVE_FAST_MODEL,
504
- settings: this.env.WAVE_FAST_MODEL,
505
503
  },
506
504
  );
507
505
  }
@@ -549,7 +547,7 @@ export class ConfigurationService {
549
547
 
550
548
  // Try env (settings.json) first, then process.env
551
549
  const envMaxInputTokens =
552
- this.env.WAVE_MAX_INPUT_TOKENS || process.env.WAVE_MAX_INPUT_TOKENS;
550
+ process.env.WAVE_MAX_INPUT_TOKENS || process.env.WAVE_MAX_INPUT_TOKENS;
553
551
  if (envMaxInputTokens) {
554
552
  const parsed = parseInt(envMaxInputTokens, 10);
555
553
  if (!isNaN(parsed)) {
@@ -599,7 +597,8 @@ export class ConfigurationService {
599
597
 
600
598
  // 2. WAVE_DISABLE_AUTO_MEMORY environment variable
601
599
  const disableAutoMemory =
602
- this.env.WAVE_DISABLE_AUTO_MEMORY || process.env.WAVE_DISABLE_AUTO_MEMORY;
600
+ process.env.WAVE_DISABLE_AUTO_MEMORY ||
601
+ process.env.WAVE_DISABLE_AUTO_MEMORY;
603
602
  if (disableAutoMemory === "1" || disableAutoMemory === "true") {
604
603
  return false;
605
604
  }
@@ -621,7 +620,7 @@ export class ConfigurationService {
621
620
 
622
621
  // 2. WAVE_AUTO_MEMORY_FREQUENCY environment variable
623
622
  const envFrequency =
624
- this.env.WAVE_AUTO_MEMORY_FREQUENCY ||
623
+ process.env.WAVE_AUTO_MEMORY_FREQUENCY ||
625
624
  process.env.WAVE_AUTO_MEMORY_FREQUENCY;
626
625
  if (envFrequency) {
627
626
  const parsed = parseInt(envFrequency, 10);
@@ -653,7 +652,7 @@ export class ConfigurationService {
653
652
 
654
653
  // Try env (settings.json) first, then process.env
655
654
  const envMaxOutputTokens =
656
- this.env.WAVE_MAX_OUTPUT_TOKENS || process.env.WAVE_MAX_OUTPUT_TOKENS;
655
+ process.env.WAVE_MAX_OUTPUT_TOKENS || process.env.WAVE_MAX_OUTPUT_TOKENS;
657
656
  if (envMaxOutputTokens) {
658
657
  const parsed = parseInt(envMaxOutputTokens, 10);
659
658
  if (!isNaN(parsed) && parsed > 0) {
@@ -680,7 +679,7 @@ export class ConfigurationService {
680
679
 
681
680
  // Add current model from options or environment
682
681
  const currentModel =
683
- this.options.model || this.env.WAVE_MODEL || process.env.WAVE_MODEL;
682
+ this.options.model || process.env.WAVE_MODEL || process.env.WAVE_MODEL;
684
683
  if (currentModel) {
685
684
  models.add(currentModel);
686
685
  }
@@ -192,7 +192,9 @@ export class InitializationService {
192
192
  transcriptPath: messageManager.getTranscriptPath(),
193
193
  cwd: workdir,
194
194
  worktreeName: agentOptions.worktreeName,
195
- env: configurationService.getEnvironmentVars(),
195
+ env: Object.fromEntries(
196
+ Object.entries(process.env).filter((e) => e[1] !== undefined),
197
+ ) as Record<string, string>,
196
198
  });
197
199
 
198
200
  // Process hook results
@@ -33,7 +33,6 @@ export class InteractionService {
33
33
  slashCommandManager,
34
34
  hookManager,
35
35
  workdir,
36
- configurationService,
37
36
  logger,
38
37
  aiManager,
39
38
  } = context;
@@ -83,7 +82,9 @@ export class InteractionService {
83
82
  transcriptPath: messageManager.getTranscriptPath(),
84
83
  cwd: workdir,
85
84
  userPrompt: content,
86
- env: configurationService.getEnvironmentVars(), // Include configuration environment variables
85
+ env: Object.fromEntries(
86
+ Object.entries(process.env).filter((e) => e[1] !== undefined),
87
+ ) as Record<string, string>, // Include environment variables
87
88
  },
88
89
  );
89
90
 
@@ -0,0 +1,107 @@
1
+ import { logger } from "../utils/globalLogger.js";
2
+ import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
3
+ import { ENTER_PLAN_MODE_TOOL_NAME } from "../constants/tools.js";
4
+ import { OPERATION_CANCELLED_BY_USER } from "../types/permissions.js";
5
+
6
+ /**
7
+ * Enter Plan Mode Tool Plugin
8
+ */
9
+ export const enterPlanModeTool: ToolPlugin = {
10
+ name: ENTER_PLAN_MODE_TOOL_NAME,
11
+ config: {
12
+ type: "function",
13
+ function: {
14
+ name: ENTER_PLAN_MODE_TOOL_NAME,
15
+ description:
16
+ "Request to enter plan mode for complex tasks requiring user approval before coding",
17
+ parameters: {
18
+ type: "object",
19
+ properties: {},
20
+ required: [],
21
+ additionalProperties: false,
22
+ },
23
+ },
24
+ },
25
+ prompt:
26
+ () => `Use this tool to proactively request entering plan mode when a task is non-trivial and benefits from planning before implementation.
27
+
28
+ ## When to Use This Tool
29
+ - Multi-file changes or refactoring that requires architectural decisions
30
+ - New features with multiple implementation steps or design trade-offs
31
+ - Tasks where you want user approval on approach before writing code
32
+ - Complex bug fixes that span multiple components
33
+
34
+ ## When NOT to Use This Tool
35
+ - Simple fixes (typos, small logic changes in a single file)
36
+ - Research tasks (searching files, reading code, gathering information)
37
+ - Tasks where the implementation approach is straightforward and unambiguous
38
+ - When already in plan mode
39
+
40
+ ## Examples
41
+
42
+ 1. Task: "Refactor the authentication system to use OAuth2" - Use EnterPlanMode to propose an approach before implementation.
43
+ 2. Task: "Add input validation to the login form" - Do NOT use EnterPlanMode, just implement it.
44
+ 3. Task: "Migrate the database schema to support multi-tenancy" - Use EnterPlanMode to plan the migration steps.
45
+ 4. Task: "Fix the off-by-one error in the pagination logic" - Do NOT use EnterPlanMode, just fix the bug.
46
+ `,
47
+ execute: async (
48
+ _args: Record<string, unknown>,
49
+ context: ToolContext,
50
+ ): Promise<ToolResult> => {
51
+ try {
52
+ if (!context.permissionManager) {
53
+ return {
54
+ success: false,
55
+ content: "",
56
+ error: "Permission manager is not available",
57
+ };
58
+ }
59
+
60
+ const permissionContext = context.permissionManager.createContext(
61
+ ENTER_PLAN_MODE_TOOL_NAME,
62
+ context.permissionMode || "default",
63
+ context.canUseToolCallback,
64
+ {},
65
+ context.toolCallId,
66
+ );
67
+
68
+ // No "allow always" option for plan mode transitions (matching Claude Code behavior)
69
+ permissionContext.hidePersistentOption = true;
70
+
71
+ const permissionResult =
72
+ await context.permissionManager.checkPermission(permissionContext);
73
+
74
+ if (permissionResult.behavior === "deny") {
75
+ if (permissionResult.message === OPERATION_CANCELLED_BY_USER) {
76
+ return {
77
+ success: false,
78
+ content: OPERATION_CANCELLED_BY_USER,
79
+ };
80
+ }
81
+ return {
82
+ success: false,
83
+ content: `User declined to enter plan mode. Proceed in current mode.`,
84
+ error: permissionResult.message
85
+ ? undefined
86
+ : "Operation declined by user",
87
+ };
88
+ }
89
+
90
+ return {
91
+ success: true,
92
+ content:
93
+ "Plan mode entered successfully. Please write your plan to the plan file and use ExitPlanMode when ready for approval.",
94
+ shortResult: "Plan mode entered",
95
+ };
96
+ } catch (error) {
97
+ const errorMessage =
98
+ error instanceof Error ? error.message : String(error);
99
+ logger.error(`EnterPlanMode tool error: ${errorMessage}`);
100
+ return {
101
+ success: false,
102
+ content: "",
103
+ error: errorMessage,
104
+ };
105
+ }
106
+ },
107
+ };
@@ -32,7 +32,9 @@ export type MessageBlock =
32
32
  export interface TextBlock {
33
33
  type: "text";
34
34
  content: string;
35
+ customCommandContent?: string;
35
36
  source?: MessageSource;
37
+ stage?: "streaming" | "end";
36
38
  }
37
39
 
38
40
  export interface ErrorBlock {
@@ -77,7 +79,7 @@ export interface BangBlock {
77
79
  type: "bang";
78
80
  command: string;
79
81
  output: string;
80
- isRunning: boolean;
82
+ stage: "running" | "end";
81
83
  exitCode: number | null;
82
84
  }
83
85
 
@@ -90,6 +92,7 @@ export interface CompressBlock {
90
92
  export interface ReasoningBlock {
91
93
  type: "reasoning";
92
94
  content: string;
95
+ stage?: "streaming" | "end";
93
96
  }
94
97
 
95
98
  export interface FileHistoryBlock {
@@ -12,6 +12,7 @@ import {
12
12
  EDIT_TOOL_NAME,
13
13
  BASH_TOOL_NAME,
14
14
  WRITE_TOOL_NAME,
15
+ ENTER_PLAN_MODE_TOOL_NAME,
15
16
  EXIT_PLAN_MODE_TOOL_NAME,
16
17
  ASK_USER_QUESTION_TOOL_NAME,
17
18
  } from "../constants/tools.js";
@@ -68,6 +69,7 @@ export const RESTRICTED_TOOLS = [
68
69
  EDIT_TOOL_NAME,
69
70
  BASH_TOOL_NAME,
70
71
  WRITE_TOOL_NAME,
72
+ ENTER_PLAN_MODE_TOOL_NAME,
71
73
  EXIT_PLAN_MODE_TOOL_NAME,
72
74
  ASK_USER_QUESTION_TOOL_NAME,
73
75
  ] as const;
@@ -111,9 +111,7 @@ export function setupAgentContainer(
111
111
  container.register("MessageManager", messageManager);
112
112
 
113
113
  const resolvedTaskListId =
114
- configurationService.getEnvironmentVars().WAVE_TASK_LIST_ID ||
115
- process.env.WAVE_TASK_LIST_ID ||
116
- messageManager.getRootSessionId();
114
+ process.env.WAVE_TASK_LIST_ID || messageManager.getRootSessionId();
117
115
 
118
116
  const taskManager = new TaskManager(container, resolvedTaskListId);
119
117
  container.register("TaskManager", taskManager);
@@ -189,7 +187,9 @@ export function setupAgentContainer(
189
187
  cwd: workdir,
190
188
  toolName: context.toolName,
191
189
  toolInput: context.toolInput,
192
- env: configurationService.getEnvironmentVars(),
190
+ env: Object.fromEntries(
191
+ Object.entries(process.env).filter((e) => e[1] !== undefined),
192
+ ) as Record<string, string>,
193
193
  });
194
194
 
195
195
  if (results.length > 0) {
@@ -191,9 +191,10 @@ export function convertMessagesForAPI(
191
191
  block.content &&
192
192
  block.content.trim().length > 0
193
193
  ) {
194
+ const textForApi = block.customCommandContent ?? block.content;
194
195
  contentParts.push({
195
196
  type: "text",
196
- text: block.content,
197
+ text: textForApi,
197
198
  });
198
199
  }
199
200