wave-agent-sdk 0.16.12 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/builtin/skills/settings/MCP.md +49 -4
- package/builtin/skills/settings/PERMISSIONS.md +31 -0
- package/dist/managers/aiManager.d.ts +19 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +338 -210
- package/dist/managers/backgroundTaskManager.js +1 -1
- package/dist/managers/bangManager.js +1 -1
- package/dist/managers/hookManager.d.ts +22 -0
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +97 -18
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +53 -41
- package/dist/managers/messageManager.d.ts +4 -0
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +9 -0
- package/dist/managers/permissionManager.d.ts +6 -0
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +14 -0
- package/dist/managers/planManager.d.ts.map +1 -1
- package/dist/managers/planManager.js +10 -0
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +28 -3
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +14 -0
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +4 -0
- package/dist/prompts/index.d.ts +0 -4
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +0 -3
- package/dist/prompts/planModeReminders.d.ts +6 -0
- package/dist/prompts/planModeReminders.d.ts.map +1 -0
- package/dist/prompts/planModeReminders.js +112 -0
- package/dist/services/aiService.d.ts +1 -0
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +3 -1
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +5 -3
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +13 -12
- package/dist/services/jsonlHandler.d.ts +1 -1
- package/dist/services/jsonlHandler.d.ts.map +1 -1
- package/dist/services/jsonlHandler.js +22 -7
- package/dist/services/session.d.ts +3 -2
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +30 -13
- package/dist/tools/agentTool.js +1 -1
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +8 -12
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +21 -8
- package/dist/tools/exitPlanMode.d.ts.map +1 -1
- package/dist/tools/exitPlanMode.js +2 -0
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +19 -4
- package/dist/tools/types.d.ts +2 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/types/agent.d.ts +4 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +5 -1
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +2 -0
- package/dist/types/mcp.d.ts +1 -0
- package/dist/types/mcp.d.ts.map +1 -1
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +6 -1
- package/dist/utils/editUtils.d.ts +3 -2
- package/dist/utils/editUtils.d.ts.map +1 -1
- package/dist/utils/editUtils.js +5 -3
- package/package.json +2 -2
- package/src/managers/aiManager.ts +420 -256
- package/src/managers/backgroundTaskManager.ts +1 -1
- package/src/managers/bangManager.ts +1 -1
- package/src/managers/hookManager.ts +125 -21
- package/src/managers/mcpManager.ts +65 -49
- package/src/managers/messageManager.ts +10 -0
- package/src/managers/permissionManager.ts +18 -0
- package/src/managers/planManager.ts +11 -0
- package/src/managers/pluginManager.ts +52 -6
- package/src/managers/slashCommandManager.ts +17 -0
- package/src/managers/subagentManager.ts +4 -0
- package/src/prompts/index.ts +0 -8
- package/src/prompts/planModeReminders.ts +138 -0
- package/src/services/aiService.ts +4 -1
- package/src/services/configurationService.ts +5 -3
- package/src/services/initializationService.ts +16 -15
- package/src/services/jsonlHandler.ts +27 -7
- package/src/services/session.ts +33 -13
- package/src/tools/agentTool.ts +1 -1
- package/src/tools/bashTool.ts +8 -11
- package/src/tools/editTool.ts +25 -8
- package/src/tools/exitPlanMode.ts +3 -0
- package/src/tools/readTool.ts +23 -5
- package/src/tools/types.ts +2 -0
- package/src/types/agent.ts +4 -0
- package/src/types/hooks.ts +9 -1
- package/src/types/mcp.ts +1 -0
- package/src/utils/containerSetup.ts +7 -3
- package/src/utils/editUtils.ts +6 -3
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ASK_USER_QUESTION_TOOL_NAME,
|
|
3
|
+
EDIT_TOOL_NAME,
|
|
4
|
+
WRITE_TOOL_NAME,
|
|
5
|
+
EXIT_PLAN_MODE_TOOL_NAME,
|
|
6
|
+
AGENT_TOOL_NAME,
|
|
7
|
+
} from "../constants/tools.js";
|
|
8
|
+
import {
|
|
9
|
+
EXPLORE_SUBAGENT_TYPE,
|
|
10
|
+
PLAN_SUBAGENT_TYPE,
|
|
11
|
+
} from "../constants/subagents.js";
|
|
12
|
+
|
|
13
|
+
export function wrapInSystemReminder(content: string): string {
|
|
14
|
+
return `<system-reminder>\n${content}\n</system-reminder>`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildPlanModeReminder(
|
|
18
|
+
planFilePath: string,
|
|
19
|
+
planExists: boolean,
|
|
20
|
+
isSubagent: boolean = false,
|
|
21
|
+
): string {
|
|
22
|
+
const planFileInfo = planExists
|
|
23
|
+
? `A plan file already exists at ${planFilePath}. You can read it and make incremental edits using the ${EDIT_TOOL_NAME} tool if you need to.`
|
|
24
|
+
: `No plan file exists yet. You should create your plan at ${planFilePath} using the ${WRITE_TOOL_NAME} tool if you need to.`;
|
|
25
|
+
|
|
26
|
+
const subagentPlanFileInfo = planExists
|
|
27
|
+
? `A plan file already exists at ${planFilePath}. You can read it for context if needed.`
|
|
28
|
+
: `No plan file exists yet.`;
|
|
29
|
+
|
|
30
|
+
if (isSubagent) {
|
|
31
|
+
return wrapInSystemReminder(`Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received (for example, to make edits). Instead, your role is to explore the codebase and return your findings as text output. Do NOT attempt to write or edit any files — the parent agent will write the plan file based on your text response.
|
|
32
|
+
|
|
33
|
+
## Plan File Info:
|
|
34
|
+
${subagentPlanFileInfo}
|
|
35
|
+
Answer the user's query comprehensively, using the ${ASK_USER_QUESTION_TOOL_NAME} tool if you need to ask the user clarifying questions. If you do use the ${ASK_USER_QUESTION_TOOL_NAME}, make sure to ask all clarifying questions you need to fully understand the user's intent before proceeding.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return wrapInSystemReminder(`Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including making configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
|
|
39
|
+
|
|
40
|
+
## Plan File Info:
|
|
41
|
+
${planFileInfo}
|
|
42
|
+
You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
|
|
43
|
+
|
|
44
|
+
## Plan Workflow
|
|
45
|
+
|
|
46
|
+
### Phase 1: Initial Understanding
|
|
47
|
+
Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. Critical: In this phase you should only use the ${AGENT_TOOL_NAME} tool with subagent_type=${EXPLORE_SUBAGENT_TYPE}.
|
|
48
|
+
|
|
49
|
+
1. Focus on understanding the user's request and the code associated with their request. Actively search for existing functions, utilities, and patterns that can be reused — avoid proposing new code when suitable implementations already exist.
|
|
50
|
+
|
|
51
|
+
2. **Launch up to 3 ${EXPLORE_SUBAGENT_TYPE} agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase.
|
|
52
|
+
- Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change.
|
|
53
|
+
- Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning.
|
|
54
|
+
- Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1)
|
|
55
|
+
- If using multiple agents: Provide each agent with a specific search focus or area to explore. Example: One agent searches for existing implementations, another explores related components, a third investigating testing patterns
|
|
56
|
+
|
|
57
|
+
### Phase 2: Design
|
|
58
|
+
Goal: Design an implementation approach.
|
|
59
|
+
|
|
60
|
+
Launch agent(s) with subagent_type=${PLAN_SUBAGENT_TYPE} to design the implementation based on the user's intent and your exploration results from Phase 1.
|
|
61
|
+
|
|
62
|
+
You can launch up to 3 agent(s) in parallel.
|
|
63
|
+
|
|
64
|
+
**Guidelines:**
|
|
65
|
+
- **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives
|
|
66
|
+
- **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames)
|
|
67
|
+
- **Multiple agents**: Use up to 3 agents for complex tasks that benefit from different perspectives
|
|
68
|
+
|
|
69
|
+
Examples of when to use multiple agents:
|
|
70
|
+
- The task touches multiple parts of the codebase
|
|
71
|
+
- It's a large refactor or architectural change
|
|
72
|
+
- There are many edge cases to consider
|
|
73
|
+
- You'd benefit from exploring different approaches
|
|
74
|
+
|
|
75
|
+
Example perspectives by task type:
|
|
76
|
+
- New feature: simplicity vs performance vs maintainability
|
|
77
|
+
- Bug fix: root cause vs workaround vs prevention
|
|
78
|
+
- Refactoring: minimal change vs clean architecture
|
|
79
|
+
|
|
80
|
+
In the agent prompt:
|
|
81
|
+
- Provide comprehensive background context from Phase 1 exploration including filenames and code path traces
|
|
82
|
+
- Describe requirements and constraints
|
|
83
|
+
- Request a detailed implementation plan
|
|
84
|
+
|
|
85
|
+
### Phase 3: Review
|
|
86
|
+
Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions.
|
|
87
|
+
1. Read the critical files identified by agents to deepen your understanding
|
|
88
|
+
2. Ensure that the plans align with the user's original request
|
|
89
|
+
3. Use ${ASK_USER_QUESTION_TOOL_NAME} to clarify any remaining questions with the user
|
|
90
|
+
|
|
91
|
+
### Phase 4: Final Plan
|
|
92
|
+
Goal: Write your final plan to the plan file (the only file you can edit).
|
|
93
|
+
- Begin with a **Context** section: explain why this change is being made — the problem or need it addresses, what prompted it, and the intended outcome
|
|
94
|
+
- Include only your recommended approach, not all alternatives
|
|
95
|
+
- Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively
|
|
96
|
+
- Include the paths of critical files to be modified
|
|
97
|
+
- Reference existing functions and utilities you found that should be reused, with their file paths
|
|
98
|
+
- Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests)
|
|
99
|
+
|
|
100
|
+
### Phase 5: Call ${EXIT_PLAN_MODE_TOOL_NAME}
|
|
101
|
+
At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call ${EXIT_PLAN_MODE_TOOL_NAME} to indicate to the user that you are done planning.
|
|
102
|
+
This is critical - your turn should only end with either using the ${ASK_USER_QUESTION_TOOL_NAME} tool OR calling ${EXIT_PLAN_MODE_TOOL_NAME}. Do not stop unless it's for these 2 reasons
|
|
103
|
+
|
|
104
|
+
**Important:** Use ${ASK_USER_QUESTION_TOOL_NAME} ONLY to clarify requirements or choose between approaches. Use ${EXIT_PLAN_MODE_TOOL_NAME} to request plan approval. Do NOT ask about plan approval in any other way - no text questions, no AskUserQuestion. Phrases like "Is this plan okay?", "Should I proceed?", "How does this plan look?", "Any changes before we start?", or similar MUST use ${EXIT_PLAN_MODE_TOOL_NAME}.
|
|
105
|
+
|
|
106
|
+
NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications using the ${ASK_USER_QUESTION_TOOL_NAME} tool. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildPlanModeSparseReminder(planFilePath: string): string {
|
|
110
|
+
return wrapInSystemReminder(
|
|
111
|
+
`Plan mode still active (see full instructions earlier in conversation). Read-only except plan file at ${planFilePath}. End turns with ${ASK_USER_QUESTION_TOOL_NAME} or ${EXIT_PLAN_MODE_TOOL_NAME}.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function buildPlanModeReEntryReminder(planFilePath: string): string {
|
|
116
|
+
return wrapInSystemReminder(`## Re-entering Plan Mode
|
|
117
|
+
|
|
118
|
+
You are returning to plan mode after having previously exited it. A plan file exists at ${planFilePath} from your previous planning session.
|
|
119
|
+
|
|
120
|
+
**Before proceeding with any new planning, you should:**
|
|
121
|
+
1. Read the existing plan file to understand what was previously planned
|
|
122
|
+
2. Evaluate the user's current request against that plan
|
|
123
|
+
3. Decide how to proceed:
|
|
124
|
+
- **Different task**: If the user's request is for a different task—even if it's similar or related—start fresh by overwriting the existing plan
|
|
125
|
+
- **Same task, continuing**: If this is explicitly a continuation or refinement of the exact same task, modify the existing plan while cleaning up outdated or irrelevant sections
|
|
126
|
+
4. Continue on with the plan process and most importantly you should always edit the plan file one way or the other before calling ${EXIT_PLAN_MODE_TOOL_NAME}
|
|
127
|
+
|
|
128
|
+
Treat this as a fresh planning session. Do not assume the existing plan is relevant without evaluating it first.`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function buildExitedPlanModeReminder(
|
|
132
|
+
planFilePath?: string,
|
|
133
|
+
planExists?: boolean,
|
|
134
|
+
): string {
|
|
135
|
+
return wrapInSystemReminder(`## Exited Plan Mode
|
|
136
|
+
|
|
137
|
+
You have exited plan mode. You can now make edits, run tools, and take actions.${planExists ? ` The plan file is located at ${planFilePath} if you need to reference it.` : ""}`);
|
|
138
|
+
}
|
|
@@ -758,6 +758,7 @@ export interface CompactMessagesOptions {
|
|
|
758
758
|
messages: ChatCompletionMessageParam[];
|
|
759
759
|
abortSignal?: AbortSignal;
|
|
760
760
|
model?: string;
|
|
761
|
+
customInstructions?: string;
|
|
761
762
|
}
|
|
762
763
|
|
|
763
764
|
export interface CompactMessagesResult {
|
|
@@ -835,7 +836,9 @@ export async function compactMessages(
|
|
|
835
836
|
...cleanedMessages,
|
|
836
837
|
{
|
|
837
838
|
role: "user",
|
|
838
|
-
content:
|
|
839
|
+
content: options.customInstructions
|
|
840
|
+
? `Please create a detailed summary of the conversation so far. Pay special attention to these instructions: ${options.customInstructions}`
|
|
841
|
+
: `Please create a detailed summary of the conversation so far.`,
|
|
839
842
|
},
|
|
840
843
|
],
|
|
841
844
|
},
|
|
@@ -511,12 +511,14 @@ export class ConfigurationService {
|
|
|
511
511
|
maxTokens?: number,
|
|
512
512
|
permissionMode?: PermissionMode,
|
|
513
513
|
): ModelConfig {
|
|
514
|
-
// Resolve agent model: override > options >
|
|
514
|
+
// Resolve agent model: override > options > currentConfiguration (settings.json model, possibly remote-merged) > process.env
|
|
515
|
+
// Priority: user's explicit model field > admin's env.WAVE_MODEL default.
|
|
516
|
+
// If admin wants hard enforcement, they set the `model` scalar field (overwrites local in mergeRemoteSettings).
|
|
515
517
|
const resolvedAgentModel =
|
|
516
518
|
model ||
|
|
517
519
|
this.options.model ||
|
|
518
|
-
|
|
519
|
-
|
|
520
|
+
this.currentConfiguration?.model ||
|
|
521
|
+
process.env.WAVE_MODEL;
|
|
520
522
|
|
|
521
523
|
// Resolve fast model: override > options > process.env (includes settings.json env)
|
|
522
524
|
const resolvedFastModel =
|
|
@@ -77,6 +77,9 @@ export class InitializationService {
|
|
|
77
77
|
|
|
78
78
|
const startTime = performance.now();
|
|
79
79
|
|
|
80
|
+
// Set global logger early so managers can use it during initialization
|
|
81
|
+
setGlobalLogger(logger || null);
|
|
82
|
+
|
|
80
83
|
// Initialize managers first
|
|
81
84
|
try {
|
|
82
85
|
const phaseStart = performance.now();
|
|
@@ -123,6 +126,19 @@ export class InitializationService {
|
|
|
123
126
|
// Don't throw error to prevent app startup failure
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
// Initialize remote settings (load disk cache synchronously, then fetch in background)
|
|
130
|
+
// Must happen BEFORE loadMergedConfiguration so remote env vars are available
|
|
131
|
+
try {
|
|
132
|
+
const phaseStart = performance.now();
|
|
133
|
+
await remoteSettingsService.initialize();
|
|
134
|
+
logger?.debug(
|
|
135
|
+
`Initialization Phase [Remote Settings] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
|
|
136
|
+
);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
logger?.error("Failed to initialize remote settings:", error);
|
|
139
|
+
// Don't throw error to prevent app startup failure - continue without remote settings
|
|
140
|
+
}
|
|
141
|
+
|
|
126
142
|
// Initialize hooks configuration
|
|
127
143
|
try {
|
|
128
144
|
const phaseStart = performance.now();
|
|
@@ -263,9 +279,6 @@ export class InitializationService {
|
|
|
263
279
|
logger?.error("Failed to initialize auto-memory directory:", error);
|
|
264
280
|
}
|
|
265
281
|
|
|
266
|
-
// Set global logger for SDK-wide access before discovering rules
|
|
267
|
-
setGlobalLogger(logger || null);
|
|
268
|
-
|
|
269
282
|
// Discover modular memory rules
|
|
270
283
|
try {
|
|
271
284
|
const phaseStart = performance.now();
|
|
@@ -289,18 +302,6 @@ export class InitializationService {
|
|
|
289
302
|
// Don't throw error to prevent app startup failure - continue without live reload
|
|
290
303
|
}
|
|
291
304
|
|
|
292
|
-
// Initialize remote settings (fetch server-managed config)
|
|
293
|
-
try {
|
|
294
|
-
const phaseStart = performance.now();
|
|
295
|
-
await remoteSettingsService.initialize();
|
|
296
|
-
logger?.debug(
|
|
297
|
-
`Initialization Phase [Remote Settings] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
|
|
298
|
-
);
|
|
299
|
-
} catch (error) {
|
|
300
|
-
logger?.error("Failed to initialize remote settings:", error);
|
|
301
|
-
// Don't throw error to prevent app startup failure - continue without remote settings
|
|
302
|
-
}
|
|
303
|
-
|
|
304
305
|
// Memory is lazy-cached on first getCombinedMemoryContent call
|
|
305
306
|
// No explicit loading needed during initialization
|
|
306
307
|
|
|
@@ -229,7 +229,19 @@ export class JsonlHandler {
|
|
|
229
229
|
// Extract filename from path
|
|
230
230
|
const filename = filePath.split("/").pop() || "";
|
|
231
231
|
|
|
232
|
-
//
|
|
232
|
+
// New timestamp-prefixed format
|
|
233
|
+
const newSubagentMatch = filename.match(
|
|
234
|
+
/^subagent-(\d{14}-[0-9a-f]{8})\.jsonl$/,
|
|
235
|
+
);
|
|
236
|
+
if (newSubagentMatch) {
|
|
237
|
+
return { sessionId: newSubagentMatch[1], sessionType: "subagent" };
|
|
238
|
+
}
|
|
239
|
+
const newMainMatch = filename.match(/^(\d{14}-[0-9a-f]{8})\.jsonl$/);
|
|
240
|
+
if (newMainMatch) {
|
|
241
|
+
return { sessionId: newMainMatch[1], sessionType: "main" };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Old UUID format (backward compat)
|
|
233
245
|
const subagentMatch = filename.match(
|
|
234
246
|
/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
235
247
|
);
|
|
@@ -240,7 +252,6 @@ export class JsonlHandler {
|
|
|
240
252
|
};
|
|
241
253
|
}
|
|
242
254
|
|
|
243
|
-
// Check if it's a main session
|
|
244
255
|
const mainMatch = filename.match(
|
|
245
256
|
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
246
257
|
);
|
|
@@ -260,18 +271,26 @@ export class JsonlHandler {
|
|
|
260
271
|
* @returns True if valid, false otherwise
|
|
261
272
|
*/
|
|
262
273
|
isValidSessionFilename(filename: string): boolean {
|
|
263
|
-
//
|
|
274
|
+
// New timestamp-prefixed format patterns
|
|
275
|
+
const newFormatPattern = /^(\d{14}-[0-9a-f]{8})\.jsonl$/;
|
|
276
|
+
const newSubagentPattern = /^subagent-(\d{14}-[0-9a-f]{8})\.jsonl$/;
|
|
277
|
+
// Old UUID format patterns (backward compat)
|
|
264
278
|
const uuidPattern =
|
|
265
279
|
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/;
|
|
266
280
|
const subagentPattern =
|
|
267
281
|
/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/;
|
|
268
282
|
|
|
269
|
-
return
|
|
283
|
+
return (
|
|
284
|
+
newFormatPattern.test(filename) ||
|
|
285
|
+
newSubagentPattern.test(filename) ||
|
|
286
|
+
uuidPattern.test(filename) ||
|
|
287
|
+
subagentPattern.test(filename)
|
|
288
|
+
);
|
|
270
289
|
}
|
|
271
290
|
|
|
272
291
|
/**
|
|
273
292
|
* Generate simple filename for sessions
|
|
274
|
-
* @param sessionId - UUID
|
|
293
|
+
* @param sessionId - Session identifier (timestamp-prefixed or legacy UUID format)
|
|
275
294
|
* @param sessionType - Type of session ("main" or "subagent")
|
|
276
295
|
* @returns Generated filename
|
|
277
296
|
*/
|
|
@@ -279,10 +298,11 @@ export class JsonlHandler {
|
|
|
279
298
|
sessionId: string,
|
|
280
299
|
sessionType: "main" | "subagent",
|
|
281
300
|
): string {
|
|
282
|
-
// Validate sessionId is
|
|
301
|
+
// Validate sessionId is either new timestamp-prefixed format or legacy UUID
|
|
302
|
+
const newFormatPattern = /^\d{14}-[0-9a-f]{8}$/;
|
|
283
303
|
const uuidPattern =
|
|
284
304
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
285
|
-
if (!uuidPattern.test(sessionId)) {
|
|
305
|
+
if (!newFormatPattern.test(sessionId) && !uuidPattern.test(sessionId)) {
|
|
286
306
|
throw new Error(`Invalid session ID format: ${sessionId}`);
|
|
287
307
|
}
|
|
288
308
|
|
package/src/services/session.ts
CHANGED
|
@@ -59,11 +59,22 @@ export interface SessionIndex {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* Generate a new session ID
|
|
63
|
-
*
|
|
62
|
+
* Generate a new session ID with a timestamp prefix for sortability
|
|
63
|
+
* Format: {YYYYMMDDHHmmss}-{8hex} (e.g. 20260527143025-a1b2c3d4)
|
|
64
|
+
* @returns Timestamp-prefixed session ID string
|
|
64
65
|
*/
|
|
65
66
|
export function generateSessionId(): string {
|
|
66
|
-
|
|
67
|
+
const now = new Date();
|
|
68
|
+
const ts = [
|
|
69
|
+
now.getFullYear(),
|
|
70
|
+
String(now.getMonth() + 1).padStart(2, "0"),
|
|
71
|
+
String(now.getDate()).padStart(2, "0"),
|
|
72
|
+
String(now.getHours()).padStart(2, "0"),
|
|
73
|
+
String(now.getMinutes()).padStart(2, "0"),
|
|
74
|
+
String(now.getSeconds()).padStart(2, "0"),
|
|
75
|
+
].join("");
|
|
76
|
+
const shortId = randomUUID().slice(0, 8);
|
|
77
|
+
return `${ts}-${shortId}`;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
/**
|
|
@@ -472,15 +483,17 @@ export async function listSessionsFromJsonl(
|
|
|
472
483
|
try {
|
|
473
484
|
const filePath = join(projectDir.encodedPath, file);
|
|
474
485
|
|
|
475
|
-
// Validate main session filename format (UUID
|
|
486
|
+
// Validate main session filename format (new timestamp format or legacy UUID)
|
|
487
|
+
const newFormatMatch = file.match(/^(\d{14}-[0-9a-f]{8})\.jsonl$/);
|
|
476
488
|
const uuidMatch = file.match(
|
|
477
489
|
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
478
490
|
);
|
|
479
|
-
|
|
491
|
+
const match = newFormatMatch || uuidMatch;
|
|
492
|
+
if (!match) {
|
|
480
493
|
continue; // Skip invalid filenames
|
|
481
494
|
}
|
|
482
495
|
|
|
483
|
-
const sessionId =
|
|
496
|
+
const sessionId = match[1];
|
|
484
497
|
|
|
485
498
|
// PERFORMANCE OPTIMIZATION: Only read the last message for timestamps and tokens
|
|
486
499
|
const jsonlHandler = new JsonlHandler();
|
|
@@ -667,17 +680,24 @@ export async function cleanupExpiredSessionsFromJsonl(
|
|
|
667
680
|
);
|
|
668
681
|
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
669
682
|
const index = JSON.parse(indexContent) as SessionIndex;
|
|
683
|
+
// New timestamp-prefixed format patterns
|
|
684
|
+
const newMainMatch = file.match(/^(\d{14}-[0-9a-f]{8})\.jsonl$/);
|
|
685
|
+
const newSubagentMatch = file.match(
|
|
686
|
+
/^subagent-(\d{14}-[0-9a-f]{8})\.jsonl$/,
|
|
687
|
+
);
|
|
688
|
+
// Old UUID format patterns (backward compat)
|
|
670
689
|
const uuidMatch = file.match(
|
|
671
|
-
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
690
|
+
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
672
691
|
);
|
|
673
692
|
const subagentMatch = file.match(
|
|
674
|
-
/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
693
|
+
/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
675
694
|
);
|
|
676
|
-
const sessionId =
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
695
|
+
const sessionId =
|
|
696
|
+
newMainMatch?.[1] ??
|
|
697
|
+
newSubagentMatch?.[1] ??
|
|
698
|
+
uuidMatch?.[1] ??
|
|
699
|
+
subagentMatch?.[1] ??
|
|
700
|
+
null;
|
|
681
701
|
|
|
682
702
|
if (sessionId && index.sessions[sessionId]) {
|
|
683
703
|
delete index.sessions[sessionId];
|
package/src/tools/agentTool.ts
CHANGED
|
@@ -246,7 +246,7 @@ When using the Agent tool, you must specify a subagent_type parameter to select
|
|
|
246
246
|
resolve({
|
|
247
247
|
success: true,
|
|
248
248
|
content: backgroundMsg,
|
|
249
|
-
shortResult: `Agent started in background: ${result}`,
|
|
249
|
+
shortResult: `Agent started in background: ${result}${outputPath ? ` → ${outputPath}` : ""}`,
|
|
250
250
|
});
|
|
251
251
|
return;
|
|
252
252
|
}
|
package/src/tools/bashTool.ts
CHANGED
|
@@ -208,7 +208,7 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
208
208
|
return {
|
|
209
209
|
success: true,
|
|
210
210
|
content: backgroundMsg,
|
|
211
|
-
shortResult: `Background process ${taskId} started`,
|
|
211
|
+
shortResult: `Background process ${taskId} started${outputPath ? ` → ${outputPath}` : ""}`,
|
|
212
212
|
};
|
|
213
213
|
}
|
|
214
214
|
|
|
@@ -226,9 +226,7 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
226
226
|
stdio: "pipe",
|
|
227
227
|
detached: true,
|
|
228
228
|
cwd: context.workdir,
|
|
229
|
-
env: {
|
|
230
|
-
...process.env,
|
|
231
|
-
},
|
|
229
|
+
env: context.env || { ...process.env },
|
|
232
230
|
});
|
|
233
231
|
|
|
234
232
|
let outputBuffer = "";
|
|
@@ -434,18 +432,17 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
434
432
|
}
|
|
435
433
|
|
|
436
434
|
// If CWD changed, call the onCwdChange callback and add notification
|
|
437
|
-
let
|
|
435
|
+
let cwdMessage: string | undefined;
|
|
438
436
|
if (newCwd && newCwd !== context.workdir && context.onCwdChange) {
|
|
439
437
|
const isInSafeZone =
|
|
440
438
|
context.permissionManager?.isPathInSafeZone?.(newCwd) ?? true;
|
|
441
439
|
|
|
442
|
-
if (isInSafeZone) {
|
|
443
|
-
context.onCwdChange(newCwd);
|
|
444
|
-
} else if (context.originalWorkdir) {
|
|
440
|
+
if (!isInSafeZone && context.originalWorkdir) {
|
|
445
441
|
context.onCwdChange(context.originalWorkdir);
|
|
446
|
-
|
|
442
|
+
cwdMessage = `Shell cwd was reset to ${context.originalWorkdir}`;
|
|
447
443
|
} else {
|
|
448
444
|
context.onCwdChange(newCwd);
|
|
445
|
+
cwdMessage = `Shell working directory changed to ${newCwd}`;
|
|
449
446
|
}
|
|
450
447
|
}
|
|
451
448
|
|
|
@@ -453,9 +450,9 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
453
450
|
const combinedOutput =
|
|
454
451
|
outputBuffer + (errorBuffer ? "\n" + errorBuffer : "");
|
|
455
452
|
|
|
456
|
-
// Prepend CWD
|
|
453
|
+
// Prepend CWD change message to output if present
|
|
457
454
|
const finalOutput =
|
|
458
|
-
(
|
|
455
|
+
(cwdMessage ? cwdMessage + "\n" : "") +
|
|
459
456
|
(combinedOutput || `Command executed with exit code: ${exitCode}`);
|
|
460
457
|
const content = processToolResult(
|
|
461
458
|
finalOutput,
|
package/src/tools/editTool.ts
CHANGED
|
@@ -30,6 +30,7 @@ Usage:
|
|
|
30
30
|
- When editing text from ${READ_TOOL_NAME} tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
|
|
31
31
|
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
32
32
|
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
|
|
33
|
+
- Use the smallest \`old_string\` that's clearly unique — usually 2-4 adjacent lines is sufficient. Avoid including 10+ lines of context when less uniquely identifies the target. Shorter matches are less likely to contain reproduction errors.
|
|
33
34
|
- The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
|
|
34
35
|
- Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.`,
|
|
35
36
|
config: {
|
|
@@ -109,6 +110,18 @@ Usage:
|
|
|
109
110
|
// Touch file to track it in context
|
|
110
111
|
context.messageManager?.touchFile(filePath);
|
|
111
112
|
|
|
113
|
+
// Enforce read-before-edit: the file must have been read first
|
|
114
|
+
if (
|
|
115
|
+
context.messageManager &&
|
|
116
|
+
!context.messageManager.hasFileInContext(filePath)
|
|
117
|
+
) {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
content: "",
|
|
121
|
+
error: `You must read the file with the ${READ_TOOL_NAME} tool before editing it. Use ${READ_TOOL_NAME} on ${filePath} first.`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
112
125
|
try {
|
|
113
126
|
const resolvedPath = resolvePath(filePath, context.workdir);
|
|
114
127
|
|
|
@@ -124,19 +137,23 @@ Usage:
|
|
|
124
137
|
};
|
|
125
138
|
}
|
|
126
139
|
|
|
140
|
+
// Normalize line endings for matching
|
|
141
|
+
const normalizedContent = originalContent.replace(/\r\n/g, "\n");
|
|
142
|
+
const normalizedOldString = oldString.replace(/\r\n/g, "\n");
|
|
143
|
+
|
|
127
144
|
// Check if old_string exists
|
|
128
|
-
const index =
|
|
129
|
-
const matchedOldString = index !== -1 ?
|
|
145
|
+
const index = normalizedContent.indexOf(normalizedOldString);
|
|
146
|
+
const matchedOldString = index !== -1 ? normalizedOldString : null;
|
|
130
147
|
const startLineNumber =
|
|
131
148
|
index !== -1
|
|
132
|
-
?
|
|
149
|
+
? normalizedContent.substring(0, index).split("\n").length
|
|
133
150
|
: undefined;
|
|
134
151
|
|
|
135
152
|
if (!matchedOldString) {
|
|
136
153
|
return {
|
|
137
154
|
success: false,
|
|
138
155
|
content: "",
|
|
139
|
-
error: analyzeEditMismatch(),
|
|
156
|
+
error: analyzeEditMismatch(normalizedOldString),
|
|
140
157
|
};
|
|
141
158
|
}
|
|
142
159
|
|
|
@@ -146,11 +163,11 @@ Usage:
|
|
|
146
163
|
if (replaceAll) {
|
|
147
164
|
// Replace all matches
|
|
148
165
|
const regex = new RegExp(escapeRegExp(matchedOldString), "g");
|
|
149
|
-
newContent =
|
|
150
|
-
replacementCount = (
|
|
166
|
+
newContent = normalizedContent.replace(regex, newString);
|
|
167
|
+
replacementCount = (normalizedContent.match(regex) || []).length;
|
|
151
168
|
} else {
|
|
152
169
|
// Replace only the first match, but first check if it's unique
|
|
153
|
-
const matches =
|
|
170
|
+
const matches = normalizedContent.split(matchedOldString).length - 1;
|
|
154
171
|
if (matches > 1) {
|
|
155
172
|
return {
|
|
156
173
|
success: false,
|
|
@@ -159,7 +176,7 @@ Usage:
|
|
|
159
176
|
};
|
|
160
177
|
}
|
|
161
178
|
|
|
162
|
-
newContent =
|
|
179
|
+
newContent = normalizedContent.replace(matchedOldString, newString);
|
|
163
180
|
replacementCount = 1;
|
|
164
181
|
}
|
|
165
182
|
|
|
@@ -107,6 +107,9 @@ Ensure your plan is complete and unambiguous:
|
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
context.permissionManager.setHasExitedPlanMode(true);
|
|
111
|
+
context.permissionManager.setNeedsPlanModeExitAttachment(true);
|
|
112
|
+
|
|
110
113
|
return {
|
|
111
114
|
success: true,
|
|
112
115
|
content: "Plan approved. Exiting plan mode.",
|
package/src/tools/readTool.ts
CHANGED
|
@@ -145,7 +145,7 @@ Usage:
|
|
|
145
145
|
- The file_path parameter must be an absolute path, not a relative path
|
|
146
146
|
- By default, it reads up to 2000 lines starting from the beginning of the file
|
|
147
147
|
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
|
|
148
|
-
-
|
|
148
|
+
- If the file content exceeds the token limit, use offset and limit parameters to read specific portions of the file, or use Bash with grep/jq to extract specific content
|
|
149
149
|
- Results are returned using cat -n format, with line numbers starting at 1
|
|
150
150
|
- This tool allows Agent to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Agent is a multimodal LLM.
|
|
151
151
|
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
|
|
@@ -341,13 +341,31 @@ Usage:
|
|
|
341
341
|
const formattedContent = selectedLines
|
|
342
342
|
.map((line, index) => {
|
|
343
343
|
const lineNumber = startLine + index;
|
|
344
|
-
|
|
345
|
-
const truncatedLine =
|
|
346
|
-
line.length > 2000 ? line.substring(0, 2000) + "..." : line;
|
|
347
|
-
return `${formatLineNumberPrefix(lineNumber)}${truncatedLine}`;
|
|
344
|
+
return `${formatLineNumberPrefix(lineNumber)}${line}`;
|
|
348
345
|
})
|
|
349
346
|
.join("\n");
|
|
350
347
|
|
|
348
|
+
// Token-level validation: estimate tokens and reject if over limit
|
|
349
|
+
const maxTokens = context.fileReadingLimits?.maxTokens ?? 25000; // Default 25000 tokens
|
|
350
|
+
const ext = extname(actualFilePath).toLowerCase().slice(1);
|
|
351
|
+
const bytesPerToken =
|
|
352
|
+
ext === "json" || ext === "jsonl" || ext === "jsonc" ? 2 : 4;
|
|
353
|
+
const estimatedTokens = Math.ceil(
|
|
354
|
+
formattedContent.length / bytesPerToken,
|
|
355
|
+
);
|
|
356
|
+
if (estimatedTokens > maxTokens) {
|
|
357
|
+
return {
|
|
358
|
+
success: false,
|
|
359
|
+
content: "",
|
|
360
|
+
error: `File content (~${estimatedTokens.toLocaleString()} tokens) exceeds maximum allowed tokens (${maxTokens.toLocaleString()}). Use offset and limit parameters to read specific portions of the file, or use Bash with grep/jq to search within it for specific content.`,
|
|
361
|
+
metadata: {
|
|
362
|
+
type: "error_token_limit_exceeded",
|
|
363
|
+
estimatedTokens,
|
|
364
|
+
maxTokens,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
351
369
|
// Add file information header
|
|
352
370
|
let content = `File: ${filePath}\n`;
|
|
353
371
|
if (startLine > 1 || endLine < totalLines) {
|
package/src/tools/types.ts
CHANGED
|
@@ -111,4 +111,6 @@ export interface ToolContext {
|
|
|
111
111
|
onCwdChange?: (newCwd: string) => void;
|
|
112
112
|
/** Original working directory (before any cd changes) for CWD reset */
|
|
113
113
|
originalWorkdir?: string;
|
|
114
|
+
/** Merged environment variables (process.env + agent env) for child processes */
|
|
115
|
+
env?: Record<string, string>;
|
|
114
116
|
}
|
package/src/types/agent.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface AgentOptions {
|
|
|
30
30
|
/** Wave server URL for SSO authentication (fallback to WAVE_SERVER_URL env var) */
|
|
31
31
|
serverUrl?: string;
|
|
32
32
|
defaultHeaders?: Record<string, string>;
|
|
33
|
+
/** Per-subagent-type headers, merged into defaultHeaders for matching subagents */
|
|
34
|
+
subagentHeaders?: Record<string, Record<string, string>>;
|
|
33
35
|
fetchOptions?: ClientOptions["fetchOptions"];
|
|
34
36
|
fetch?: ClientOptions["fetch"];
|
|
35
37
|
model?: string;
|
|
@@ -95,6 +97,8 @@ export interface AgentOptions {
|
|
|
95
97
|
* File-based hooks (from config.json/.waverc.json) merge on top of these.
|
|
96
98
|
*/
|
|
97
99
|
hooks?: PartialHookConfiguration;
|
|
100
|
+
/** Per-agent environment variables, merged on top of process.env for bash, MCP, and hooks */
|
|
101
|
+
env?: Record<string, string>;
|
|
98
102
|
[key: string]: unknown;
|
|
99
103
|
}
|
|
100
104
|
|
package/src/types/hooks.ts
CHANGED
|
@@ -24,7 +24,9 @@ export type HookEvent =
|
|
|
24
24
|
| "WorktreeRemove"
|
|
25
25
|
| "CwdChanged"
|
|
26
26
|
| "SessionStart"
|
|
27
|
-
| "SessionEnd"
|
|
27
|
+
| "SessionEnd"
|
|
28
|
+
| "PreCompact"
|
|
29
|
+
| "PostCompact";
|
|
28
30
|
|
|
29
31
|
// Individual hook command configuration
|
|
30
32
|
export interface HookCommand {
|
|
@@ -117,6 +119,8 @@ export function isValidHookEvent(event: string): event is HookEvent {
|
|
|
117
119
|
"CwdChanged",
|
|
118
120
|
"SessionStart",
|
|
119
121
|
"SessionEnd",
|
|
122
|
+
"PreCompact",
|
|
123
|
+
"PostCompact",
|
|
120
124
|
].includes(event);
|
|
121
125
|
}
|
|
122
126
|
|
|
@@ -187,6 +191,8 @@ export interface HookJsonInput {
|
|
|
187
191
|
source?: SessionStartSource; // Present for SessionStart events
|
|
188
192
|
agent_type?: string; // Present for SessionStart events
|
|
189
193
|
end_source?: SessionEndSource; // Present for SessionEnd events
|
|
194
|
+
compact_instructions?: string; // Present for PreCompact events
|
|
195
|
+
compact_summary?: string; // Present for PostCompact events
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
// Extended context interface for passing additional data to hook executor
|
|
@@ -205,6 +211,8 @@ export interface ExtendedHookExecutionContext extends HookExecutionContext {
|
|
|
205
211
|
source?: SessionStartSource; // Session start source (SessionStart only)
|
|
206
212
|
agentType?: string; // Agent type identifier (SessionStart only)
|
|
207
213
|
endSource?: SessionEndSource; // Session end source (SessionEnd only)
|
|
214
|
+
compactInstructions?: string; // Custom instructions for PreCompact
|
|
215
|
+
compactSummary?: string; // Summary text for PostCompact
|
|
208
216
|
}
|
|
209
217
|
|
|
210
218
|
// Environment variables injected into hook processes
|