wave-agent-sdk 0.15.0 → 0.15.2

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 (144) hide show
  1. package/builtin/skills/loop/SKILL.md +29 -3
  2. package/dist/agent.d.ts +11 -2
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +44 -11
  5. package/dist/constants/tools.d.ts +3 -0
  6. package/dist/constants/tools.d.ts.map +1 -1
  7. package/dist/constants/tools.js +3 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -0
  11. package/dist/managers/aiManager.d.ts +13 -1
  12. package/dist/managers/aiManager.d.ts.map +1 -1
  13. package/dist/managers/aiManager.js +69 -17
  14. package/dist/managers/hookManager.d.ts.map +1 -1
  15. package/dist/managers/hookManager.js +9 -0
  16. package/dist/managers/mcpManager.d.ts +4 -1
  17. package/dist/managers/mcpManager.d.ts.map +1 -1
  18. package/dist/managers/mcpManager.js +25 -5
  19. package/dist/managers/messageManager.d.ts.map +1 -1
  20. package/dist/managers/messageManager.js +7 -6
  21. package/dist/managers/permissionManager.d.ts +0 -2
  22. package/dist/managers/permissionManager.d.ts.map +1 -1
  23. package/dist/managers/permissionManager.js +0 -30
  24. package/dist/managers/slashCommandManager.d.ts +1 -0
  25. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  26. package/dist/managers/slashCommandManager.js +20 -4
  27. package/dist/managers/subagentManager.d.ts +6 -1
  28. package/dist/managers/subagentManager.d.ts.map +1 -1
  29. package/dist/managers/subagentManager.js +17 -18
  30. package/dist/managers/toolManager.d.ts +6 -0
  31. package/dist/managers/toolManager.d.ts.map +1 -1
  32. package/dist/managers/toolManager.js +41 -1
  33. package/dist/prompts/index.d.ts +1 -2
  34. package/dist/prompts/index.d.ts.map +1 -1
  35. package/dist/prompts/index.js +14 -6
  36. package/dist/services/initializationService.d.ts +0 -2
  37. package/dist/services/initializationService.d.ts.map +1 -1
  38. package/dist/services/initializationService.js +3 -35
  39. package/dist/services/jsonlHandler.d.ts +4 -4
  40. package/dist/services/jsonlHandler.d.ts.map +1 -1
  41. package/dist/services/jsonlHandler.js +4 -13
  42. package/dist/services/memory.d.ts +6 -0
  43. package/dist/services/memory.d.ts.map +1 -1
  44. package/dist/services/memory.js +27 -14
  45. package/dist/services/session.d.ts.map +1 -1
  46. package/dist/services/session.js +3 -12
  47. package/dist/tools/agentTool.d.ts.map +1 -1
  48. package/dist/tools/agentTool.js +16 -4
  49. package/dist/tools/bashTool.d.ts.map +1 -1
  50. package/dist/tools/bashTool.js +2 -5
  51. package/dist/tools/cronCreateTool.d.ts.map +1 -1
  52. package/dist/tools/cronCreateTool.js +71 -6
  53. package/dist/tools/cronDeleteTool.d.ts.map +1 -1
  54. package/dist/tools/cronDeleteTool.js +5 -1
  55. package/dist/tools/cronListTool.d.ts.map +1 -1
  56. package/dist/tools/cronListTool.js +5 -1
  57. package/dist/tools/enterWorktreeTool.d.ts +8 -0
  58. package/dist/tools/enterWorktreeTool.d.ts.map +1 -0
  59. package/dist/tools/enterWorktreeTool.js +144 -0
  60. package/dist/tools/exitWorktreeTool.d.ts +8 -0
  61. package/dist/tools/exitWorktreeTool.d.ts.map +1 -0
  62. package/dist/tools/exitWorktreeTool.js +184 -0
  63. package/dist/tools/skillTool.d.ts.map +1 -1
  64. package/dist/tools/skillTool.js +16 -4
  65. package/dist/tools/taskManagementTools.d.ts.map +1 -1
  66. package/dist/tools/taskManagementTools.js +4 -0
  67. package/dist/tools/toolSearchTool.d.ts +15 -0
  68. package/dist/tools/toolSearchTool.d.ts.map +1 -0
  69. package/dist/tools/toolSearchTool.js +185 -0
  70. package/dist/tools/types.d.ts +19 -0
  71. package/dist/tools/types.d.ts.map +1 -1
  72. package/dist/tools/webFetchTool.d.ts.map +1 -1
  73. package/dist/tools/webFetchTool.js +1 -0
  74. package/dist/types/agent.d.ts +6 -1
  75. package/dist/types/agent.d.ts.map +1 -1
  76. package/dist/types/hooks.d.ts +3 -1
  77. package/dist/types/hooks.d.ts.map +1 -1
  78. package/dist/types/hooks.js +1 -0
  79. package/dist/types/messaging.d.ts +1 -0
  80. package/dist/types/messaging.d.ts.map +1 -1
  81. package/dist/types/session.d.ts +0 -4
  82. package/dist/types/session.d.ts.map +1 -1
  83. package/dist/utils/containerSetup.d.ts.map +1 -1
  84. package/dist/utils/containerSetup.js +4 -6
  85. package/dist/utils/cronToHuman.d.ts +6 -0
  86. package/dist/utils/cronToHuman.d.ts.map +1 -0
  87. package/dist/utils/cronToHuman.js +79 -0
  88. package/dist/utils/isDeferredTool.d.ts +19 -0
  89. package/dist/utils/isDeferredTool.d.ts.map +1 -0
  90. package/dist/utils/isDeferredTool.js +31 -0
  91. package/dist/utils/mcpUtils.d.ts.map +1 -1
  92. package/dist/utils/mcpUtils.js +1 -0
  93. package/dist/utils/messageOperations.d.ts.map +1 -1
  94. package/dist/utils/messageOperations.js +5 -0
  95. package/dist/utils/parseCronExpression.d.ts +6 -0
  96. package/dist/utils/parseCronExpression.d.ts.map +1 -0
  97. package/dist/utils/parseCronExpression.js +74 -0
  98. package/dist/utils/worktreeSession.d.ts +26 -0
  99. package/dist/utils/worktreeSession.d.ts.map +1 -0
  100. package/dist/utils/worktreeSession.js +14 -0
  101. package/dist/utils/worktreeUtils.d.ts +42 -0
  102. package/dist/utils/worktreeUtils.d.ts.map +1 -0
  103. package/dist/utils/worktreeUtils.js +236 -0
  104. package/package.json +1 -1
  105. package/src/agent.ts +61 -12
  106. package/src/constants/tools.ts +3 -0
  107. package/src/index.ts +1 -0
  108. package/src/managers/aiManager.ts +73 -18
  109. package/src/managers/hookManager.ts +10 -0
  110. package/src/managers/mcpManager.ts +32 -6
  111. package/src/managers/messageManager.ts +7 -8
  112. package/src/managers/permissionManager.ts +0 -42
  113. package/src/managers/slashCommandManager.ts +30 -5
  114. package/src/managers/subagentManager.ts +28 -23
  115. package/src/managers/toolManager.ts +47 -1
  116. package/src/prompts/index.ts +17 -6
  117. package/src/services/initializationService.ts +2 -41
  118. package/src/services/jsonlHandler.ts +12 -24
  119. package/src/services/memory.ts +30 -17
  120. package/src/services/session.ts +3 -14
  121. package/src/tools/agentTool.ts +24 -5
  122. package/src/tools/bashTool.ts +2 -5
  123. package/src/tools/cronCreateTool.ts +81 -8
  124. package/src/tools/cronDeleteTool.ts +7 -2
  125. package/src/tools/cronListTool.ts +7 -2
  126. package/src/tools/enterWorktreeTool.ts +183 -0
  127. package/src/tools/exitWorktreeTool.ts +242 -0
  128. package/src/tools/skillTool.ts +24 -4
  129. package/src/tools/taskManagementTools.ts +4 -0
  130. package/src/tools/toolSearchTool.ts +228 -0
  131. package/src/tools/types.ts +19 -0
  132. package/src/tools/webFetchTool.ts +1 -0
  133. package/src/types/agent.ts +6 -0
  134. package/src/types/hooks.ts +4 -0
  135. package/src/types/messaging.ts +1 -0
  136. package/src/types/session.ts +0 -8
  137. package/src/utils/containerSetup.ts +7 -8
  138. package/src/utils/cronToHuman.ts +99 -0
  139. package/src/utils/isDeferredTool.ts +36 -0
  140. package/src/utils/mcpUtils.ts +1 -0
  141. package/src/utils/messageOperations.ts +5 -0
  142. package/src/utils/parseCronExpression.ts +78 -0
  143. package/src/utils/worktreeSession.ts +36 -0
  144. package/src/utils/worktreeUtils.ts +288 -0
@@ -24,7 +24,7 @@ interface McpConnection {
24
24
  }
25
25
 
26
26
  export interface McpManagerCallbacks {
27
- onServersChange?: (servers: McpServerStatus[]) => void;
27
+ onMcpServersChange?: (servers: McpServerStatus[]) => void;
28
28
  }
29
29
 
30
30
  import { logger } from "../utils/globalLogger.js";
@@ -32,6 +32,8 @@ import { logger } from "../utils/globalLogger.js";
32
32
  export interface McpManagerOptions {
33
33
  callbacks?: McpManagerCallbacks;
34
34
  logger?: Logger;
35
+ /** Pre-configured MCP servers passed from constructor options */
36
+ mcpServers?: Record<string, McpServerConfig>;
35
37
  }
36
38
 
37
39
  /**
@@ -96,12 +98,14 @@ export class McpManager {
96
98
  private configPath: string = "";
97
99
  private workdir: string = "";
98
100
  private callbacks: McpManagerCallbacks;
101
+ private mcpServers: Record<string, McpServerConfig> | undefined;
99
102
 
100
103
  constructor(
101
104
  private container: Container,
102
105
  options: McpManagerOptions = {},
103
106
  ) {
104
107
  this.callbacks = options.callbacks || {};
108
+ this.mcpServers = options.mcpServers;
105
109
  }
106
110
 
107
111
  /**
@@ -114,11 +118,20 @@ export class McpManager {
114
118
  this.configPath = join(workdir, ".mcp.json");
115
119
  this.workdir = workdir;
116
120
 
121
+ // Register constructor-provided servers before loading .mcp.json
122
+ if (this.mcpServers) {
123
+ for (const [name, config] of Object.entries(this.mcpServers)) {
124
+ this.addServer(name, config);
125
+ }
126
+ }
127
+
117
128
  if (autoConnect) {
118
129
  logger?.debug("Initializing MCP servers...");
119
130
 
120
- // Ensure MCP configuration is loaded
121
- const config = await this.ensureConfigLoaded();
131
+ // Load workspace MCP configuration (always read, merge with any plugin servers already added)
132
+ await this.loadConfig();
133
+
134
+ const config = this.config;
122
135
 
123
136
  if (config && config.mcpServers) {
124
137
  // Connect to all configured servers in background to avoid blocking agent initialization
@@ -145,7 +158,7 @@ export class McpManager {
145
158
 
146
159
  logger?.debug("MCP servers initialization started in background");
147
160
  // Trigger state change callback after starting initialization
148
- this.callbacks.onServersChange?.(this.getAllServers());
161
+ this.callbacks.onMcpServersChange?.(this.getAllServers());
149
162
  }
150
163
  }
151
164
 
@@ -164,7 +177,20 @@ export class McpManager {
164
177
 
165
178
  try {
166
179
  const configContent = await fs.readFile(this.configPath, "utf-8");
167
- this.config = resolveMcpConfig(JSON.parse(configContent));
180
+ const workspaceConfig = resolveMcpConfig(JSON.parse(configContent));
181
+
182
+ // Merge workspace config with any existing config (e.g., from plugins or constructor)
183
+ // Constructor-provided servers take precedence, then workspace config, then existing config
184
+ const merged: McpConfig = { mcpServers: {} };
185
+ if (this.config) {
186
+ Object.assign(merged.mcpServers, this.config.mcpServers);
187
+ }
188
+ Object.assign(merged.mcpServers, workspaceConfig.mcpServers);
189
+ // Constructor-provided servers override both for same names
190
+ if (this.mcpServers) {
191
+ Object.assign(merged.mcpServers, this.mcpServers);
192
+ }
193
+ this.config = merged;
168
194
 
169
195
  // Initialize server statuses (preserve existing status for already known servers)
170
196
  if (this.config) {
@@ -226,7 +252,7 @@ export class McpManager {
226
252
  if (server) {
227
253
  this.servers.set(name, { ...server, ...updates });
228
254
  // Trigger state change callback
229
- this.callbacks.onServersChange?.(this.getAllServers());
255
+ this.callbacks.onMcpServersChange?.(this.getAllServers());
230
256
  }
231
257
  }
232
258
 
@@ -518,6 +518,7 @@ export class MessageManager {
518
518
  sessionId: this.sessionId,
519
519
  },
520
520
  ],
521
+ timestamp: new Date().toISOString(),
521
522
  ...(usage && { usage }),
522
523
  };
523
524
 
@@ -959,15 +960,13 @@ export class MessageManager {
959
960
  try {
960
961
  const { writeFile } = await import("fs/promises");
961
962
 
962
- const sessionMessages: import("../types/session.js").SessionMessage[] =
963
- messages.map((message) => ({
964
- ...message,
965
- timestamp: new Date().toISOString(),
966
- }));
967
-
968
963
  const content =
969
- sessionMessages.map((m) => JSON.stringify(m)).join("\n") +
970
- (sessionMessages.length > 0 ? "\n" : "");
964
+ messages
965
+ .map((m) => {
966
+ const { timestamp, ...rest } = m;
967
+ return JSON.stringify({ timestamp, ...rest });
968
+ })
969
+ .join("\n") + (messages.length > 0 ? "\n" : "");
971
970
 
972
971
  await writeFile(this.transcriptPath, content, "utf8");
973
972
  } catch (error) {
@@ -129,8 +129,6 @@ export class PermissionManager {
129
129
  private additionalDirectories: string[] = [];
130
130
  private systemAdditionalDirectories: string[] = [];
131
131
  private planFilePath?: string;
132
- private worktreeName?: string;
133
- private mainRepoRoot?: string;
134
132
  private workdir?: string;
135
133
  private onConfiguredPermissionModeChange?: (mode: PermissionMode) => void;
136
134
  private _logger?: Logger;
@@ -151,8 +149,6 @@ export class PermissionManager {
151
149
  this.addSystemAdditionalDirectory(dir);
152
150
  }
153
151
 
154
- this.worktreeName = this.container.get<string>("WorktreeName");
155
- this.mainRepoRoot = this.container.get<string>("MainRepoRoot");
156
152
  this.workdir = this.container.get<string>("Workdir");
157
153
  }
158
154
 
@@ -438,44 +434,6 @@ export class PermissionManager {
438
434
  }
439
435
  }
440
436
 
441
- // 0. Check worktree safety for Write and Edit tools
442
- const currentWorkdir = this.getWorkdir();
443
- if (
444
- this.worktreeName &&
445
- this.mainRepoRoot &&
446
- currentWorkdir &&
447
- (context.toolName === WRITE_TOOL_NAME ||
448
- context.toolName === EDIT_TOOL_NAME)
449
- ) {
450
- const targetPath = context.toolInput?.file_path as string | undefined;
451
- if (targetPath) {
452
- const absoluteTargetPath = path.resolve(currentWorkdir, targetPath);
453
- const isInsideMainRepo = isPathInside(
454
- absoluteTargetPath,
455
- this.mainRepoRoot,
456
- );
457
- const isInsideWorktree = isPathInside(
458
- absoluteTargetPath,
459
- currentWorkdir,
460
- );
461
-
462
- // If it's inside the main repo but NOT inside the current worktree
463
- if (isInsideMainRepo && !isInsideWorktree) {
464
- logger?.warn("Worktree safety violation", {
465
- toolName: context.toolName,
466
- targetPath,
467
- worktreeName: this.worktreeName,
468
- mainRepoRoot: this.mainRepoRoot,
469
- workdir: currentWorkdir,
470
- });
471
- return {
472
- behavior: "deny",
473
- message: `Access denied: You are currently in a worktree session ("${this.worktreeName}"). Modifying files in the main repository (outside the worktree) is not allowed. Please only modify files within the worktree directory: ${currentWorkdir}`,
474
- };
475
- }
476
- }
477
- }
478
-
479
437
  // 0. Check denied rules first - Deny always takes precedence
480
438
  for (const rule of this.deniedRules) {
481
439
  if (this.matchesRule(context, rule)) {
@@ -23,6 +23,7 @@ import {
23
23
  import type { SkillManager } from "./skillManager.js";
24
24
  import type { SkillMetadata } from "../types/skills.js";
25
25
  import type { SubagentManager } from "./subagentManager.js";
26
+ import type { MemoryService } from "../services/memory.js";
26
27
 
27
28
  import { logger } from "../utils/globalLogger.js";
28
29
 
@@ -81,6 +82,10 @@ export class SlashCommandManager {
81
82
  return this.container.get<SubagentManager>("SubagentManager")!;
82
83
  }
83
84
 
85
+ private get memoryService(): MemoryService {
86
+ return this.container.get<MemoryService>("MemoryService")!;
87
+ }
88
+
84
89
  private initializeBuiltinCommands(): void {
85
90
  // Register built-in clear command
86
91
  this.registerCommand({
@@ -90,6 +95,7 @@ export class SlashCommandManager {
90
95
  handler: async () => {
91
96
  this.aiManager.abortAIMessage();
92
97
  this.messageManager.clearMessages();
98
+ this.memoryService.clearCache();
93
99
  await this.taskManager.syncWithSession();
94
100
  },
95
101
  });
@@ -224,20 +230,39 @@ export class SlashCommandManager {
224
230
  const messages = subagent.messages;
225
231
  const tokens =
226
232
  subagent.messageManager.getLatestTotalTokens();
227
- const lastTools = subagent.lastTools;
233
+ const usedTools = subagent.usedTools;
228
234
 
229
235
  const toolCount = countToolBlocks(messages);
230
236
  const summary = formatToolTokenSummary(toolCount, tokens);
231
237
 
238
+ const getDisplayParam = (t: {
239
+ name: string;
240
+ parameters: string;
241
+ compactParams?: string;
242
+ stage?: string;
243
+ }) => {
244
+ if (
245
+ (t.stage === "end" || t.stage === "running") &&
246
+ t.compactParams
247
+ ) {
248
+ return t.compactParams;
249
+ }
250
+ const flat = t.parameters.replace(/\n/g, "\\n");
251
+ return flat.length > 30 ? `…${flat.slice(-30)}` : flat;
252
+ };
253
+
232
254
  let shortResult = "";
233
255
  if (toolCount > 2) {
234
256
  shortResult += "... ";
235
257
  }
236
- if (lastTools.length > 0) {
237
- shortResult += `${lastTools.join(", ")} `;
238
- }
239
-
240
258
  shortResult += summary;
259
+ if (usedTools.length > 0) {
260
+ shortResult +=
261
+ "\n" +
262
+ usedTools
263
+ .map((t) => `${t.name} ${getDisplayParam(t)}`)
264
+ .join("\n");
265
+ }
241
266
 
242
267
  this.messageManager.updateToolBlock({
243
268
  id: toolBlockId,
@@ -68,7 +68,12 @@ export interface SubagentInstance {
68
68
  toolManager: ToolManager;
69
69
  status: "initializing" | "active" | "completed" | "error" | "aborted";
70
70
  messages: Message[];
71
- lastTools: string[]; // Track last two tool names
71
+ usedTools: {
72
+ name: string;
73
+ parameters: string;
74
+ compactParams?: string;
75
+ stage?: string;
76
+ }[]; // Track tools with display info
72
77
  subagentType: string; // Store the subagent type for hook context
73
78
  description: string; // Store the AI-generated description
74
79
  allowedTools?: string[]; // Optional permission rules (e.g. git:*)
@@ -388,7 +393,7 @@ export class SubagentManager {
388
393
  toolManager,
389
394
  status: "initializing",
390
395
  messages: [],
391
- lastTools: [], // Initialize lastTools
396
+ usedTools: [], // Initialize usedTools
392
397
  subagentType: parameters.subagent_type, // Store the subagent type
393
398
  description: parameters.description, // Store the AI-generated description
394
399
  allowedTools: parameters.allowedTools, // Store optional permission rules
@@ -790,27 +795,16 @@ export class SubagentManager {
790
795
  },
791
796
 
792
797
  onToolBlockUpdated: (params: AgentToolBlockUpdateParams) => {
793
- // Track last two tool names when a tool starts running
794
- if (params.stage === "running" && params.name) {
795
- const instance = this.instances.get(subagentId);
796
- if (instance) {
797
- // Add to lastTools if it's different from the last one or we want to show duplicates
798
- // Based on "ToolA, ToolB" requirement, we'll just keep the last two
799
- instance.lastTools.push(params.name);
800
- if (instance.lastTools.length > 2) {
801
- instance.lastTools.shift();
802
- }
803
- instance.onUpdate?.();
804
-
805
- // Log tool execution to file
806
- if (instance.logStream) {
807
- const displayParams =
808
- params.compactParams ||
809
- (params.parameters || "{}").substring(0, 100);
810
- instance.logStream.write(
811
- `[${new Date().toISOString()}] ${params.name}${displayParams ? ` ${displayParams}` : ""}\n`,
812
- );
813
- }
798
+ const instance = this.instances.get(subagentId);
799
+ if (instance) {
800
+ // Log tool execution to file only when finalized
801
+ if (instance.logStream && params.stage === "end") {
802
+ const displayParams =
803
+ params.compactParams ||
804
+ (params.parameters || "").substring(0, 100);
805
+ instance.logStream.write(
806
+ `[${new Date().toISOString()}] ${params.name}${displayParams ? ` ${displayParams}` : ""}\n`,
807
+ );
814
808
  }
815
809
  }
816
810
 
@@ -825,6 +819,17 @@ export class SubagentManager {
825
819
  const instance = this.instances.get(subagentId);
826
820
  if (instance) {
827
821
  instance.messages = messages;
822
+ // Compute usedTools from messages (last 2 tool blocks)
823
+ const toolBlocks = messages.flatMap(
824
+ (m) => m.blocks?.filter((b) => b.type === "tool") ?? [],
825
+ );
826
+ const last2 = toolBlocks.slice(-2);
827
+ instance.usedTools = last2.map((tb) => ({
828
+ name: tb.name ?? "",
829
+ parameters: tb.parameters ?? "",
830
+ compactParams: tb.compactParams,
831
+ stage: tb.stage,
832
+ }));
828
833
  // Trigger the onUpdate callback if provided
829
834
  instance.onUpdate?.();
830
835
  // Forward subagent message changes to parent via callbacks
@@ -23,6 +23,8 @@ import {
23
23
  taskUpdateTool,
24
24
  taskListTool,
25
25
  } from "../tools/taskManagementTools.js";
26
+ import { enterWorktreeTool } from "../tools/enterWorktreeTool.js";
27
+ import { exitWorktreeTool } from "../tools/exitWorktreeTool.js";
26
28
  import { McpManager } from "./mcpManager.js";
27
29
  import { PermissionManager } from "./permissionManager.js";
28
30
  import { ChatCompletionFunctionTool } from "openai/resources.js";
@@ -43,6 +45,8 @@ import { logger } from "../utils/globalLogger.js";
43
45
 
44
46
  import type { SubagentConfiguration } from "../utils/subagentParser.js";
45
47
  import type { SkillMetadata } from "../types/skills.js";
48
+ import { toolSearchTool } from "../tools/toolSearchTool.js";
49
+ import { isDeferredTool } from "../utils/isDeferredTool.js";
46
50
 
47
51
  export interface ToolManagerOptions {
48
52
  container: Container;
@@ -123,6 +127,9 @@ class ToolManager {
123
127
  cronDeleteTool,
124
128
  cronListTool,
125
129
  webFetchTool,
130
+ enterWorktreeTool,
131
+ exitWorktreeTool,
132
+ toolSearchTool,
126
133
  ];
127
134
 
128
135
  for (const tool of builtInTools) {
@@ -191,6 +198,7 @@ class ToolManager {
191
198
  permissionMode: effectivePermissionMode,
192
199
  canUseToolCallback,
193
200
  permissionManager,
201
+ toolManager: this, // Allow ToolSearchTool to access the tool manager
194
202
  taskManager:
195
203
  this.container.get<import("../services/taskManager.js").TaskManager>(
196
204
  "TaskManager",
@@ -224,6 +232,11 @@ class ToolManager {
224
232
  this.container.get<import("./messageManager.js").MessageManager>(
225
233
  "MessageManager",
226
234
  )!,
235
+ hookManager: this.container.has("HookManager")
236
+ ? this.container.get<import("./hookManager.js").HookManager>(
237
+ "HookManager",
238
+ )
239
+ : undefined,
227
240
  sessionId: context.sessionId,
228
241
  toolCallId: context.toolCallId,
229
242
  };
@@ -287,10 +300,13 @@ class ToolManager {
287
300
  availableSkills?: SkillMetadata[];
288
301
  workdir?: string;
289
302
  isSubagent?: boolean;
303
+ /** Set of discovered deferred tool names to include in the API call */
304
+ discoveredTools?: Set<string>;
290
305
  }): ChatCompletionFunctionTool[] {
291
306
  const permissionManager =
292
307
  this.container.get<PermissionManager>("PermissionManager");
293
308
  const effectivePermissionMode = this.getPermissionMode();
309
+ const discoveredTools = options?.discoveredTools;
294
310
  const builtInToolsConfig = Array.from(this.toolsRegistry.values())
295
311
  .filter((tool) => {
296
312
  // If tool is explicitly denied by name in permission rules, filter it out
@@ -315,6 +331,10 @@ class ToolManager {
315
331
  effectivePermissionMode !== "bypassPermissions"
316
332
  );
317
333
  }
334
+ // Exclude deferred tools that haven't been discovered yet
335
+ if (isDeferredTool(tool) && !discoveredTools?.has(tool.name)) {
336
+ return false;
337
+ }
318
338
  return true;
319
339
  })
320
340
  .map((tool) => {
@@ -333,7 +353,16 @@ class ToolManager {
333
353
  });
334
354
  const mcpToolsConfig = this.mcpManager
335
355
  .getMcpToolsConfig()
336
- .filter((tool) => !permissionManager?.isToolDenied(tool.function.name));
356
+ .filter((tool) => {
357
+ if (permissionManager?.isToolDenied(tool.function.name)) {
358
+ return false;
359
+ }
360
+ // Exclude MCP tools that haven't been discovered yet
361
+ if (discoveredTools && !discoveredTools.has(tool.function.name)) {
362
+ return false;
363
+ }
364
+ return true;
365
+ });
337
366
  return [...builtInToolsConfig, ...mcpToolsConfig];
338
367
  }
339
368
 
@@ -375,6 +404,23 @@ class ToolManager {
375
404
  return this.container.get<PermissionManager>("PermissionManager");
376
405
  }
377
406
 
407
+ /**
408
+ * Get the names of all deferred tools (those that require ToolSearch to discover).
409
+ */
410
+ public getDeferredToolNames(): string[] {
411
+ const permissionManager =
412
+ this.container.get<PermissionManager>("PermissionManager");
413
+ const builtInDeferred = Array.from(this.toolsRegistry.values())
414
+ .filter((tool) => isDeferredTool(tool))
415
+ .filter((tool) => !permissionManager?.isToolDenied(tool.name))
416
+ .map((tool) => tool.name);
417
+ const mcpDeferred = this.mcpManager
418
+ .getMcpToolsConfig()
419
+ .filter((tool) => !permissionManager?.isToolDenied(tool.function.name))
420
+ .map((tool) => tool.function.name);
421
+ return [...builtInDeferred, ...mcpDeferred];
422
+ }
423
+
378
424
  /**
379
425
  * Get the task manager
380
426
  */
@@ -1,6 +1,7 @@
1
1
  import * as os from "node:os";
2
2
  import { ToolPlugin } from "../tools/types.js";
3
3
  import { isGitRepository } from "../utils/gitUtils.js";
4
+ import { getCurrentWorktreeSession } from "../utils/worktreeSession.js";
4
5
  import { buildAutoMemoryPrompt } from "./autoMemory.js";
5
6
  import { PermissionMode } from "../types/permissions.js";
6
7
  import {
@@ -17,9 +18,9 @@ import {
17
18
  READ_TOOL_NAME,
18
19
  GLOB_TOOL_NAME,
19
20
  GREP_TOOL_NAME,
21
+ TOOL_SEARCH_TOOL_NAME,
20
22
  } from "../constants/tools.js";
21
-
22
- export const MAX_PARALLEL_TOOL_CALLS = 3;
23
+ import { isDeferredTool } from "../utils/isDeferredTool.js";
23
24
 
24
25
  export const BASE_SYSTEM_PROMPT = `You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.`;
25
26
 
@@ -60,7 +61,6 @@ export const TOOL_POLICY = `# Using your tools
60
61
  - To search the content of files, use ${GREP_TOOL_NAME} instead of grep or rg
61
62
  - Reserve using the ${BASH_TOOL_NAME} exclusively for system commands and terminal operations that require shell execution. If you are unsure and there is a relevant dedicated tool, default to using the dedicated tool and only fallback on using the ${BASH_TOOL_NAME} tool for these if it is absolutely necessary.
62
63
  - You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency.
63
- - **Limit**: You MUST NOT call more than ${MAX_PARALLEL_TOOL_CALLS} tools in parallel in a single response.
64
64
  - However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.
65
65
  - If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks.`;
66
66
 
@@ -260,6 +260,13 @@ export function buildSystemPrompt(
260
260
  prompt += `\n\n${TOOL_POLICY}`;
261
261
  }
262
262
 
263
+ // List available deferred tool names so the model knows they exist
264
+ // Matching Claude Code: deferred tools appear by name, not loaded until fetched.
265
+ const deferredToolNames = tools.filter(isDeferredTool).map((t) => t.name);
266
+ if (deferredToolNames.length > 0) {
267
+ prompt += `\n\n<available-deferred-tools>${deferredToolNames.join(" ")}\nThese tools are NOT loaded yet — call ${TOOL_SEARCH_TOOL_NAME} first to discover their schemas before invoking them.</available-deferred-tools>`;
268
+ }
269
+
263
270
  prompt += `\n\n${OUTPUT_EFFICIENCY_PROMPT}`;
264
271
  prompt += `\n\n${TONE_AND_STYLE_PROMPT}`;
265
272
 
@@ -287,11 +294,13 @@ export function buildSystemPrompt(
287
294
  ? "bash"
288
295
  : shell;
289
296
 
297
+ const worktreeSession = getCurrentWorktreeSession();
298
+
290
299
  prompt += `
291
300
 
292
301
  Here is useful information about the environment you are running in:
293
302
  <env>
294
- Working directory: ${options.workdir}
303
+ Working directory: ${options.workdir}${worktreeSession ? `\nThis is a git worktree — an isolated copy of the repository. Run all commands from this directory. Do NOT \`cd\` to the original repository root at ${worktreeSession.originalCwd}.` : ""}
295
304
  Is directory a git repo: ${isGitRepo}
296
305
  Platform: ${platform}
297
306
  Shell: ${shellName}
@@ -330,8 +339,10 @@ export function enhanceSystemPromptWithEnvDetails(
330
339
  ? "bash"
331
340
  : shell;
332
341
 
342
+ const worktreeSession = getCurrentWorktreeSession();
343
+
333
344
  const notes = `Notes:
334
- - Agent threads always have their cwd reset between bash calls, as a result please only use absolute file paths.
345
+ - Agent threads always have their cwd reset between bash calls, as a result please only use absolute file paths.${worktreeSession ? `\n- You are in a git worktree at ${worktreeSession.worktreePath} (branch: ${worktreeSession.worktreeBranch}). Absolute paths from prior context may refer to the original repo at ${worktreeSession.originalCwd}; translate them to your worktree. Do NOT edit files outside this worktree.` : ""}
335
346
  - In your final response, share file paths (always absolute, never relative) that are relevant to the task. Include code snippets only when the exact text is load-bearing (e.g., a bug you found, a function signature the caller asked for) — do not recap code you merely read.
336
347
  - For clear communication with the user the assistant MUST avoid using emojis.
337
348
  - Do not use a colon before tool calls. Text like "Let me read the file:" followed by a read tool call should just be "Let me read the file." with a period.`;
@@ -342,7 +353,7 @@ ${notes}
342
353
 
343
354
  Here is useful information about the environment you are running in:
344
355
  <env>
345
- Working directory: ${workdir}
356
+ Working directory: ${workdir}${worktreeSession ? `\nThis is a git worktree — an isolated copy of the repository. Run all commands from this directory. Do NOT \`cd\` to the original repository root at ${worktreeSession.originalCwd}.` : ""}
346
357
  Is directory a git repo: ${isGitRepo}
347
358
  Platform: ${platform}
348
359
  Shell: ${shellName}
@@ -22,7 +22,6 @@ import type { MemoryRuleManager } from "../managers/MemoryRuleManager.js";
22
22
  import type { LiveConfigManager } from "../managers/liveConfigManager.js";
23
23
  import type { TaskManager } from "./taskManager.js";
24
24
  import type { PermissionManager } from "../managers/permissionManager.js";
25
- import type { MemoryService } from "./memory.js";
26
25
 
27
26
  export interface InitializationContext {
28
27
  skillManager: SkillManager;
@@ -42,8 +41,6 @@ export interface InitializationContext {
42
41
  memoryRuleManager: MemoryRuleManager;
43
42
  liveConfigManager: LiveConfigManager;
44
43
  taskManager: TaskManager;
45
- setProjectMemory: (content: string) => void;
46
- setUserMemory: (content: string) => void;
47
44
  resolveAndValidateConfig: () => void;
48
45
  }
49
46
 
@@ -74,8 +71,6 @@ export class InitializationService {
74
71
  memoryRuleManager,
75
72
  liveConfigManager,
76
73
  taskManager,
77
- setProjectMemory,
78
- setUserMemory,
79
74
  resolveAndValidateConfig,
80
75
  } = context;
81
76
 
@@ -293,42 +288,8 @@ export class InitializationService {
293
288
  // Don't throw error to prevent app startup failure - continue without live reload
294
289
  }
295
290
 
296
- // Load memory files during initialization
297
- try {
298
- const phaseStart = performance.now();
299
- const memoryService = container.get<MemoryService>("MemoryService");
300
- if (!memoryService) {
301
- throw new Error("MemoryService not found in container");
302
- }
303
-
304
- // Load project memory from AGENTS.md
305
- try {
306
- const projectMemoryContent =
307
- await memoryService.readMemoryFile(workdir);
308
- setProjectMemory(projectMemoryContent);
309
- } catch (error) {
310
- logger?.warn("Failed to load project memory file:", error);
311
- setProjectMemory("");
312
- }
313
-
314
- // Load user memory
315
- try {
316
- const userMemoryContent = await memoryService.getUserMemoryContent();
317
- setUserMemory(userMemoryContent);
318
- } catch (error) {
319
- logger?.warn("Failed to load user memory file:", error);
320
- setUserMemory("");
321
- }
322
- logger?.debug(
323
- `Initialization Phase [Memory Files Loading] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
324
- );
325
- } catch (error) {
326
- // Ensure memory is always initialized even if loading fails
327
- setProjectMemory("");
328
- setUserMemory("");
329
- logger?.error("Failed to load memory files:", error);
330
- // Don't throw error to prevent app startup failure
331
- }
291
+ // Memory is lazy-cached on first getCombinedMemoryContent call
292
+ // No explicit loading needed during initialization
332
293
 
333
294
  // Handle session restoration or set provided messages
334
295
  const sessionPhaseStart = performance.now();