wave-agent-sdk 0.15.1 → 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.
- package/builtin/skills/loop/SKILL.md +29 -3
- package/dist/agent.d.ts +7 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +34 -11
- package/dist/constants/tools.d.ts +3 -0
- package/dist/constants/tools.d.ts.map +1 -1
- package/dist/constants/tools.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts +13 -1
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +69 -17
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +9 -0
- package/dist/managers/mcpManager.d.ts +4 -1
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +25 -5
- package/dist/managers/permissionManager.d.ts +0 -2
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +0 -30
- package/dist/managers/slashCommandManager.d.ts +1 -0
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +4 -0
- package/dist/managers/toolManager.d.ts +6 -0
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +41 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +14 -4
- package/dist/services/initializationService.d.ts +0 -2
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +3 -35
- package/dist/services/memory.d.ts +6 -0
- package/dist/services/memory.d.ts.map +1 -1
- package/dist/services/memory.js +27 -14
- package/dist/tools/cronCreateTool.d.ts.map +1 -1
- package/dist/tools/cronCreateTool.js +71 -6
- package/dist/tools/cronDeleteTool.d.ts.map +1 -1
- package/dist/tools/cronDeleteTool.js +5 -1
- package/dist/tools/cronListTool.d.ts.map +1 -1
- package/dist/tools/cronListTool.js +5 -1
- package/dist/tools/enterWorktreeTool.d.ts +8 -0
- package/dist/tools/enterWorktreeTool.d.ts.map +1 -0
- package/dist/tools/enterWorktreeTool.js +144 -0
- package/dist/tools/exitWorktreeTool.d.ts +8 -0
- package/dist/tools/exitWorktreeTool.d.ts.map +1 -0
- package/dist/tools/exitWorktreeTool.js +184 -0
- package/dist/tools/taskManagementTools.d.ts.map +1 -1
- package/dist/tools/taskManagementTools.js +4 -0
- package/dist/tools/toolSearchTool.d.ts +15 -0
- package/dist/tools/toolSearchTool.d.ts.map +1 -0
- package/dist/tools/toolSearchTool.js +185 -0
- package/dist/tools/types.d.ts +19 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/webFetchTool.d.ts.map +1 -1
- package/dist/tools/webFetchTool.js +1 -0
- package/dist/types/agent.d.ts +6 -1
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +3 -1
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +1 -0
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +4 -6
- package/dist/utils/cronToHuman.d.ts +6 -0
- package/dist/utils/cronToHuman.d.ts.map +1 -0
- package/dist/utils/cronToHuman.js +79 -0
- package/dist/utils/isDeferredTool.d.ts +19 -0
- package/dist/utils/isDeferredTool.d.ts.map +1 -0
- package/dist/utils/isDeferredTool.js +31 -0
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +1 -0
- package/dist/utils/parseCronExpression.d.ts +6 -0
- package/dist/utils/parseCronExpression.d.ts.map +1 -0
- package/dist/utils/parseCronExpression.js +74 -0
- package/dist/utils/worktreeSession.d.ts +26 -0
- package/dist/utils/worktreeSession.d.ts.map +1 -0
- package/dist/utils/worktreeSession.js +14 -0
- package/dist/utils/worktreeUtils.d.ts +42 -0
- package/dist/utils/worktreeUtils.d.ts.map +1 -0
- package/dist/utils/worktreeUtils.js +236 -0
- package/package.json +1 -1
- package/src/agent.ts +49 -12
- package/src/constants/tools.ts +3 -0
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +73 -18
- package/src/managers/hookManager.ts +10 -0
- package/src/managers/mcpManager.ts +32 -6
- package/src/managers/permissionManager.ts +0 -42
- package/src/managers/slashCommandManager.ts +6 -0
- package/src/managers/toolManager.ts +47 -1
- package/src/prompts/index.ts +17 -3
- package/src/services/initializationService.ts +2 -41
- package/src/services/memory.ts +30 -17
- package/src/tools/cronCreateTool.ts +81 -8
- package/src/tools/cronDeleteTool.ts +7 -2
- package/src/tools/cronListTool.ts +7 -2
- package/src/tools/enterWorktreeTool.ts +183 -0
- package/src/tools/exitWorktreeTool.ts +242 -0
- package/src/tools/taskManagementTools.ts +4 -0
- package/src/tools/toolSearchTool.ts +228 -0
- package/src/tools/types.ts +19 -0
- package/src/tools/webFetchTool.ts +1 -0
- package/src/types/agent.ts +6 -0
- package/src/types/hooks.ts +4 -0
- package/src/utils/containerSetup.ts +7 -8
- package/src/utils/cronToHuman.ts +99 -0
- package/src/utils/isDeferredTool.ts +36 -0
- package/src/utils/mcpUtils.ts +1 -0
- package/src/utils/parseCronExpression.ts +78 -0
- package/src/utils/worktreeSession.ts +36 -0
- package/src/utils/worktreeUtils.ts +288 -0
|
@@ -24,7 +24,7 @@ interface McpConnection {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface McpManagerCallbacks {
|
|
27
|
-
|
|
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
|
-
//
|
|
121
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
255
|
+
this.callbacks.onMcpServersChange?.(this.getAllServers());
|
|
230
256
|
}
|
|
231
257
|
}
|
|
232
258
|
|
|
@@ -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
|
});
|
|
@@ -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) =>
|
|
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
|
*/
|
package/src/prompts/index.ts
CHANGED
|
@@ -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,7 +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";
|
|
23
|
+
import { isDeferredTool } from "../utils/isDeferredTool.js";
|
|
21
24
|
|
|
22
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.`;
|
|
23
26
|
|
|
@@ -257,6 +260,13 @@ export function buildSystemPrompt(
|
|
|
257
260
|
prompt += `\n\n${TOOL_POLICY}`;
|
|
258
261
|
}
|
|
259
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
|
+
|
|
260
270
|
prompt += `\n\n${OUTPUT_EFFICIENCY_PROMPT}`;
|
|
261
271
|
prompt += `\n\n${TONE_AND_STYLE_PROMPT}`;
|
|
262
272
|
|
|
@@ -284,11 +294,13 @@ export function buildSystemPrompt(
|
|
|
284
294
|
? "bash"
|
|
285
295
|
: shell;
|
|
286
296
|
|
|
297
|
+
const worktreeSession = getCurrentWorktreeSession();
|
|
298
|
+
|
|
287
299
|
prompt += `
|
|
288
300
|
|
|
289
301
|
Here is useful information about the environment you are running in:
|
|
290
302
|
<env>
|
|
291
|
-
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}.` : ""}
|
|
292
304
|
Is directory a git repo: ${isGitRepo}
|
|
293
305
|
Platform: ${platform}
|
|
294
306
|
Shell: ${shellName}
|
|
@@ -327,8 +339,10 @@ export function enhanceSystemPromptWithEnvDetails(
|
|
|
327
339
|
? "bash"
|
|
328
340
|
: shell;
|
|
329
341
|
|
|
342
|
+
const worktreeSession = getCurrentWorktreeSession();
|
|
343
|
+
|
|
330
344
|
const notes = `Notes:
|
|
331
|
-
- 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.` : ""}
|
|
332
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.
|
|
333
347
|
- For clear communication with the user the assistant MUST avoid using emojis.
|
|
334
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.`;
|
|
@@ -339,7 +353,7 @@ ${notes}
|
|
|
339
353
|
|
|
340
354
|
Here is useful information about the environment you are running in:
|
|
341
355
|
<env>
|
|
342
|
-
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}.` : ""}
|
|
343
357
|
Is directory a git repo: ${isGitRepo}
|
|
344
358
|
Platform: ${platform}
|
|
345
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
|
-
//
|
|
297
|
-
|
|
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();
|
package/src/services/memory.ts
CHANGED
|
@@ -8,8 +8,26 @@ import { getGitCommonDir } from "../utils/gitUtils.js";
|
|
|
8
8
|
import { pathEncoder } from "../utils/pathEncoder.js";
|
|
9
9
|
|
|
10
10
|
export class MemoryService {
|
|
11
|
+
private _cachedProjectMemory: string = "";
|
|
12
|
+
private _cachedUserMemory: string = "";
|
|
13
|
+
private _cachedCombinedMemory: string | null = null;
|
|
14
|
+
|
|
11
15
|
constructor(private container: Container) {}
|
|
12
16
|
|
|
17
|
+
public get cachedProjectMemory(): string {
|
|
18
|
+
return this._cachedProjectMemory;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public get cachedUserMemory(): string {
|
|
22
|
+
return this._cachedUserMemory;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public clearCache(): void {
|
|
26
|
+
this._cachedProjectMemory = "";
|
|
27
|
+
this._cachedUserMemory = "";
|
|
28
|
+
this._cachedCombinedMemory = null;
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
/**
|
|
14
32
|
* Get the project-specific auto-memory directory.
|
|
15
33
|
* Uses the git common directory to ensure worktrees share the same memory.
|
|
@@ -143,24 +161,19 @@ export class MemoryService {
|
|
|
143
161
|
}
|
|
144
162
|
|
|
145
163
|
async getCombinedMemoryContent(workdir: string): Promise<string> {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// Read user-level memory content
|
|
150
|
-
const userMemoryContent = await this.getUserMemoryContent();
|
|
151
|
-
|
|
152
|
-
// Merge project memory and user memory
|
|
153
|
-
let combinedMemory = "";
|
|
154
|
-
if (memoryContent.trim()) {
|
|
155
|
-
combinedMemory += memoryContent;
|
|
164
|
+
if (this._cachedCombinedMemory !== null) {
|
|
165
|
+
return this._cachedCombinedMemory;
|
|
156
166
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
167
|
+
this._cachedProjectMemory = await this.readMemoryFile(workdir);
|
|
168
|
+
this._cachedUserMemory = await this.getUserMemoryContent();
|
|
169
|
+
|
|
170
|
+
let combined = "";
|
|
171
|
+
if (this._cachedProjectMemory.trim()) combined += this._cachedProjectMemory;
|
|
172
|
+
if (this._cachedUserMemory.trim()) {
|
|
173
|
+
if (combined) combined += "\n\n";
|
|
174
|
+
combined += this._cachedUserMemory;
|
|
162
175
|
}
|
|
163
|
-
|
|
164
|
-
return
|
|
176
|
+
this._cachedCombinedMemory = combined;
|
|
177
|
+
return combined;
|
|
165
178
|
}
|
|
166
179
|
}
|
|
@@ -1,30 +1,73 @@
|
|
|
1
1
|
import { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
2
2
|
import { CRON_CREATE_TOOL_NAME } from "../constants/tools.js";
|
|
3
|
+
import { cronToHuman } from "../utils/cronToHuman.js";
|
|
4
|
+
import { parseCronExpression } from "../utils/parseCronExpression.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_AGE_DAYS = 7;
|
|
7
|
+
const MAX_JOBS = 50;
|
|
8
|
+
|
|
9
|
+
const CRON_CREATE_DESCRIPTION = `Schedule a prompt to run at a future time within this Wave session — either recurring on a cron schedule, or once at a specific time.`;
|
|
10
|
+
|
|
11
|
+
const CRON_CREATE_PROMPT = `Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.
|
|
12
|
+
|
|
13
|
+
Uses standard 5-field cron in the user's local timezone: minute hour day-of-month month day-of-week. "0 9 * * *" means 9am local — no timezone conversion needed.
|
|
14
|
+
|
|
15
|
+
## One-shot tasks (recurring: false)
|
|
16
|
+
|
|
17
|
+
For "remind me at X" or "at <time>, do Y" requests — fire once then auto-delete.
|
|
18
|
+
Pin minute/hour/day-of-month/month to specific values:
|
|
19
|
+
"remind me at 2:30pm today to check the deploy" → cron: "30 14 <today_dom> <today_month> *", recurring: false
|
|
20
|
+
"tomorrow morning, run the smoke test" → cron: "57 8 <tomorrow_dom> <tomorrow_month> *", recurring: false
|
|
21
|
+
|
|
22
|
+
## Recurring jobs (recurring: true, the default)
|
|
23
|
+
|
|
24
|
+
For "every N minutes" / "every hour" / "weekdays at 9am" requests:
|
|
25
|
+
"*/5 * * * *" (every 5 min), "0 * * * *" (hourly), "0 9 * * 1-5" (weekdays at 9am local)
|
|
26
|
+
|
|
27
|
+
## Avoid the :00 and :30 minute marks when the task allows it
|
|
28
|
+
|
|
29
|
+
Every user who asks for "9am" gets \`0 9\`, and every user who asks for "hourly" gets \`0 *\` — which means requests from across the planet land on the API at the same instant. When the user's request is approximate, pick a minute that is NOT 0 or 30:
|
|
30
|
+
"every morning around 9" → "57 8 * * *" or "3 9 * * *" (not "0 9 * * *")
|
|
31
|
+
"hourly" → "7 * * * *" (not "0 * * * *")
|
|
32
|
+
"in an hour or so, remind me to..." → pick whatever minute you land on, don't round
|
|
33
|
+
|
|
34
|
+
Only use minute 0 or 30 when the user names that exact time and clearly means it ("at 9:00 sharp", "at half past", coordinating with a meeting). When in doubt, nudge a few minutes early or late — the user will not notice, and the fleet will.
|
|
35
|
+
|
|
36
|
+
## Session-only
|
|
37
|
+
|
|
38
|
+
Jobs live only in this Wave session — nothing is written to disk, and the job is gone when Wave exits.
|
|
39
|
+
|
|
40
|
+
## Runtime behavior
|
|
41
|
+
|
|
42
|
+
Jobs only fire while the REPL is idle (not mid-query). The scheduler adds a small deterministic jitter on top of whatever you pick: recurring tasks fire up to 10% of their period late (max 15 min); one-shot tasks landing on :00 or :30 fire up to 90s early. Picking an off-minute is still the bigger lever.
|
|
43
|
+
|
|
44
|
+
Recurring tasks auto-expire after ${DEFAULT_MAX_AGE_DAYS} days — they fire one final time, then are deleted. This bounds session lifetime. Tell the user about the ${DEFAULT_MAX_AGE_DAYS}-day limit when scheduling recurring jobs.
|
|
45
|
+
|
|
46
|
+
Returns a job ID you can pass to CronDelete.`;
|
|
3
47
|
|
|
4
48
|
export const cronCreateTool: ToolPlugin = {
|
|
5
49
|
name: CRON_CREATE_TOOL_NAME,
|
|
50
|
+
shouldDefer: true,
|
|
6
51
|
config: {
|
|
7
52
|
type: "function",
|
|
8
53
|
function: {
|
|
9
54
|
name: CRON_CREATE_TOOL_NAME,
|
|
10
|
-
description:
|
|
11
|
-
"Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.",
|
|
55
|
+
description: CRON_CREATE_DESCRIPTION,
|
|
12
56
|
parameters: {
|
|
13
57
|
type: "object",
|
|
14
58
|
properties: {
|
|
15
59
|
cron: {
|
|
16
60
|
type: "string",
|
|
17
61
|
description:
|
|
18
|
-
'Standard 5-field cron expression in local time: "M H DoM Mon DoW"',
|
|
62
|
+
'Standard 5-field cron expression in local time: "M H DoM Mon DoW" (e.g. "*/5 * * * *" = every 5 minutes, "30 14 28 2 *" = Feb 28 at 2:30pm local once).',
|
|
19
63
|
},
|
|
20
64
|
prompt: {
|
|
21
65
|
type: "string",
|
|
22
|
-
description: "The prompt to enqueue at each fire time",
|
|
66
|
+
description: "The prompt to enqueue at each fire time.",
|
|
23
67
|
},
|
|
24
68
|
recurring: {
|
|
25
69
|
type: "boolean",
|
|
26
|
-
description:
|
|
27
|
-
"Default: true. true = fire on every cron match until deleted or auto-expired after 7 days. false = fire once at the next match, then auto-delete",
|
|
70
|
+
description: `true (default) = fire on every cron match until deleted or auto-expired after ${DEFAULT_MAX_AGE_DAYS} days. false = fire once at the next match, then auto-delete. Use false for "remind me at X" one-shot requests with pinned minute/hour/dom/month.`,
|
|
28
71
|
default: true,
|
|
29
72
|
},
|
|
30
73
|
},
|
|
@@ -32,6 +75,7 @@ export const cronCreateTool: ToolPlugin = {
|
|
|
32
75
|
},
|
|
33
76
|
},
|
|
34
77
|
},
|
|
78
|
+
prompt: () => CRON_CREATE_PROMPT,
|
|
35
79
|
execute: async (
|
|
36
80
|
args: Record<string, unknown>,
|
|
37
81
|
context: ToolContext,
|
|
@@ -50,6 +94,25 @@ export const cronCreateTool: ToolPlugin = {
|
|
|
50
94
|
};
|
|
51
95
|
}
|
|
52
96
|
|
|
97
|
+
// Validate cron expression
|
|
98
|
+
if (!parseCronExpression(cron)) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
content: "",
|
|
102
|
+
error: `Invalid cron expression '${cron}'. Expected 5 fields: M H DoM Mon DoW.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check max jobs limit
|
|
107
|
+
const existingJobs = context.cronManager.listJobs();
|
|
108
|
+
if (existingJobs.length >= MAX_JOBS) {
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
content: "",
|
|
112
|
+
error: `Too many scheduled jobs (max ${MAX_JOBS}). Cancel one first.`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
53
116
|
try {
|
|
54
117
|
const job = context.cronManager.createJob({
|
|
55
118
|
cron,
|
|
@@ -57,10 +120,20 @@ export const cronCreateTool: ToolPlugin = {
|
|
|
57
120
|
recurring,
|
|
58
121
|
});
|
|
59
122
|
|
|
123
|
+
const humanSchedule = cronToHuman(cron);
|
|
124
|
+
const where = "Session-only (not written to disk, dies when Wave exits)";
|
|
125
|
+
const resultMessage = recurring
|
|
126
|
+
? `Scheduled recurring job ${job.id} (${humanSchedule}). ${where}. Auto-expires after ${DEFAULT_MAX_AGE_DAYS} days. Use CronDelete to cancel sooner.`
|
|
127
|
+
: `Scheduled one-shot task ${job.id} (${humanSchedule}). ${where}. It will fire once then auto-delete.`;
|
|
128
|
+
|
|
60
129
|
return {
|
|
61
130
|
success: true,
|
|
62
|
-
content: JSON.stringify(
|
|
63
|
-
|
|
131
|
+
content: JSON.stringify(
|
|
132
|
+
{ id: job.id, humanSchedule, recurring },
|
|
133
|
+
null,
|
|
134
|
+
2,
|
|
135
|
+
),
|
|
136
|
+
shortResult: resultMessage,
|
|
64
137
|
};
|
|
65
138
|
} catch (error) {
|
|
66
139
|
return {
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
2
2
|
import { CRON_DELETE_TOOL_NAME } from "../constants/tools.js";
|
|
3
3
|
|
|
4
|
+
const CRON_DELETE_DESCRIPTION = "Cancel a scheduled cron job by ID";
|
|
5
|
+
|
|
6
|
+
const CRON_DELETE_PROMPT = `Cancel a cron job previously scheduled with CronCreate. Removes it from the in-memory session store.`;
|
|
7
|
+
|
|
4
8
|
export const cronDeleteTool: ToolPlugin = {
|
|
5
9
|
name: CRON_DELETE_TOOL_NAME,
|
|
10
|
+
shouldDefer: true,
|
|
6
11
|
config: {
|
|
7
12
|
type: "function",
|
|
8
13
|
function: {
|
|
9
14
|
name: CRON_DELETE_TOOL_NAME,
|
|
10
|
-
description:
|
|
11
|
-
"Cancel a cron job previously scheduled with CronCreate. Removes it from the in-memory session store.",
|
|
15
|
+
description: CRON_DELETE_DESCRIPTION,
|
|
12
16
|
parameters: {
|
|
13
17
|
type: "object",
|
|
14
18
|
properties: {
|
|
@@ -21,6 +25,7 @@ export const cronDeleteTool: ToolPlugin = {
|
|
|
21
25
|
},
|
|
22
26
|
},
|
|
23
27
|
},
|
|
28
|
+
prompt: () => CRON_DELETE_PROMPT,
|
|
24
29
|
execute: async (
|
|
25
30
|
args: Record<string, unknown>,
|
|
26
31
|
context: ToolContext,
|