wave-agent-sdk 0.18.3 → 0.18.5
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/ENV.md +1 -1
- package/builtin/skills/settings/MEMORY.md +76 -0
- package/builtin/skills/settings/SKILL.md +4 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +0 -2
- package/dist/managers/MemoryRuleManager.d.ts +9 -0
- package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
- package/dist/managers/MemoryRuleManager.js +18 -0
- package/dist/managers/aiManager.d.ts +1 -1
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +370 -345
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +22 -15
- package/dist/managers/messageManager.d.ts +32 -13
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +82 -76
- package/dist/prompts/index.d.ts +10 -4
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +21 -20
- package/dist/services/aiService.d.ts +2 -1
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +37 -10
- package/dist/services/memory.d.ts +1 -0
- package/dist/services/memory.d.ts.map +1 -1
- package/dist/services/memory.js +35 -17
- package/dist/tools/agentTool.d.ts.map +1 -1
- package/dist/tools/agentTool.js +24 -2
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +17 -10
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +7 -6
- package/dist/tools/readTool.js +2 -2
- package/dist/tools/writeTool.js +2 -2
- package/dist/utils/constants.d.ts +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +1 -0
- package/dist/utils/gitUtils.js +6 -6
- package/dist/utils/openaiClient.d.ts.map +1 -1
- package/dist/utils/openaiClient.js +33 -32
- package/dist/utils/path.d.ts +7 -0
- package/dist/utils/path.d.ts.map +1 -1
- package/dist/utils/path.js +9 -0
- package/dist/utils/taskReminder.d.ts +2 -3
- package/dist/utils/taskReminder.d.ts.map +1 -1
- package/dist/utils/taskReminder.js +7 -18
- package/dist/utils/worktreeUtils.d.ts.map +1 -1
- package/dist/utils/worktreeUtils.js +19 -13
- package/package.json +1 -1
- package/scripts/install_ripgrep.js +21 -1
- package/src/agent.ts +0 -5
- package/src/managers/MemoryRuleManager.ts +23 -0
- package/src/managers/aiManager.ts +473 -434
- package/src/managers/mcpManager.ts +26 -15
- package/src/managers/messageManager.ts +99 -82
- package/src/prompts/index.ts +33 -24
- package/src/services/aiService.ts +39 -12
- package/src/services/memory.ts +34 -16
- package/src/tools/agentTool.ts +24 -2
- package/src/tools/bashTool.ts +18 -9
- package/src/tools/editTool.ts +7 -9
- package/src/tools/readTool.ts +2 -2
- package/src/tools/writeTool.ts +2 -2
- package/src/utils/constants.ts +1 -1
- package/src/utils/containerSetup.ts +1 -0
- package/src/utils/gitUtils.ts +6 -6
- package/src/utils/openaiClient.ts +40 -31
- package/src/utils/path.ts +10 -0
- package/src/utils/taskReminder.ts +7 -19
- package/src/utils/worktreeUtils.ts +46 -28
- package/builtin/skills/settings/MEMORY_RULES.md +0 -60
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
|
@@ -484,29 +484,40 @@ export class McpManager {
|
|
|
484
484
|
stderr: "pipe", // Pipe stderr to capture it
|
|
485
485
|
});
|
|
486
486
|
|
|
487
|
-
//
|
|
487
|
+
// Buffer stderr silently (MCP servers use stderr for all logging;
|
|
488
|
+
// stdout is reserved for JSON-RPC). Only dump it on connection
|
|
489
|
+
// success (once) or failure — not line-by-line in real time.
|
|
490
|
+
// Aligns with Claude Code's approach to avoid noise from [INFO],
|
|
491
|
+
// [DEBUG], Warning: lines that are not actual errors.
|
|
488
492
|
const stderr = (transport as StdioClientTransport).stderr;
|
|
493
|
+
let stderrOutput = "";
|
|
489
494
|
if (stderr) {
|
|
490
|
-
let buffer = "";
|
|
491
495
|
stderr.on("data", (chunk: Buffer) => {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
buffer = lines.pop() || "";
|
|
495
|
-
for (const line of lines) {
|
|
496
|
-
if (line.trim()) {
|
|
497
|
-
logger?.error(`[MCP Server ${name}] ${line}`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
stderr.on("end", () => {
|
|
502
|
-
if (buffer.trim()) {
|
|
503
|
-
logger?.error(`[MCP Server ${name}] ${buffer}`);
|
|
496
|
+
if (stderrOutput.length < 64 * 1024 * 1024) {
|
|
497
|
+
stderrOutput += chunk.toString();
|
|
504
498
|
}
|
|
505
499
|
});
|
|
506
500
|
}
|
|
507
501
|
|
|
508
502
|
client = createClient();
|
|
509
|
-
|
|
503
|
+
try {
|
|
504
|
+
await client.connect(transport);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
if (stderrOutput.trim()) {
|
|
507
|
+
logger?.error(
|
|
508
|
+
`[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Dump accumulated stderr once after successful connection
|
|
515
|
+
if (stderrOutput.trim()) {
|
|
516
|
+
logger?.debug(
|
|
517
|
+
`[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`,
|
|
518
|
+
);
|
|
519
|
+
stderrOutput = "";
|
|
520
|
+
}
|
|
510
521
|
|
|
511
522
|
const toolsResponse = await client.listTools();
|
|
512
523
|
tools =
|
|
@@ -28,8 +28,10 @@ import {
|
|
|
28
28
|
} from "../services/session.js";
|
|
29
29
|
import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
|
|
30
30
|
import type { MemoryRuleManager } from "./MemoryRuleManager.js";
|
|
31
|
+
import type { MemoryRule } from "../types/memoryRule.js";
|
|
31
32
|
import type { MemoryService } from "../services/memory.js";
|
|
32
33
|
import { pathEncoder } from "../utils/pathEncoder.js";
|
|
34
|
+
import { READ_TOOL_NAME } from "../constants/tools.js";
|
|
33
35
|
|
|
34
36
|
import { Container } from "../utils/container.js";
|
|
35
37
|
|
|
@@ -100,7 +102,8 @@ export class MessageManager {
|
|
|
100
102
|
private callbacks: MessageManagerCallbacks;
|
|
101
103
|
private transcriptPath: string; // Cached transcript path
|
|
102
104
|
private savedMessageCount: number; // Track how many messages have been saved to prevent duplication
|
|
103
|
-
private
|
|
105
|
+
private pendingFileReadTriggers: Set<string> = new Set(); // File paths read via Read tool, awaiting rule matching
|
|
106
|
+
private loadedRuleIds: Set<string> = new Set(); // IDs of conditional rules already injected as meta messages
|
|
104
107
|
private recentFileReads: Map<string, { content: string; timestamp: number }> =
|
|
105
108
|
new Map(); // Track file read contents
|
|
106
109
|
private invokedSkills: Map<string, { skillName: string; timestamp: number }> =
|
|
@@ -172,10 +175,27 @@ export class MessageManager {
|
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
/**
|
|
175
|
-
*
|
|
178
|
+
* Process pending file read triggers: match against conditional rules,
|
|
179
|
+
* return rules not yet loaded (dedup via loadedRuleIds), mark them as loaded.
|
|
180
|
+
* Clears triggers after processing.
|
|
176
181
|
*/
|
|
177
|
-
public
|
|
178
|
-
|
|
182
|
+
public processTriggeredRules(): MemoryRule[] {
|
|
183
|
+
if (!this.memoryRuleManager || this.pendingFileReadTriggers.size === 0) {
|
|
184
|
+
this.pendingFileReadTriggers.clear();
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
const filePaths = Array.from(this.pendingFileReadTriggers);
|
|
188
|
+
this.pendingFileReadTriggers.clear();
|
|
189
|
+
|
|
190
|
+
const { conditional } =
|
|
191
|
+
this.memoryRuleManager.getActiveRulesSplit(filePaths);
|
|
192
|
+
const newRules = conditional.filter(
|
|
193
|
+
(rule) => !this.loadedRuleIds.has(rule.id),
|
|
194
|
+
);
|
|
195
|
+
for (const rule of newRules) {
|
|
196
|
+
this.loadedRuleIds.add(rule.id);
|
|
197
|
+
}
|
|
198
|
+
return newRules;
|
|
179
199
|
}
|
|
180
200
|
|
|
181
201
|
public getSessionDir(): string {
|
|
@@ -187,7 +207,8 @@ export class MessageManager {
|
|
|
187
207
|
}
|
|
188
208
|
|
|
189
209
|
/**
|
|
190
|
-
* Get combined memory content (project memory + user memory +
|
|
210
|
+
* Get combined memory content (project memory + user memory + unconditional rules)
|
|
211
|
+
* Note: conditional rules are handled separately via processTriggeredRules().
|
|
191
212
|
*/
|
|
192
213
|
public async getCombinedMemory(): Promise<string> {
|
|
193
214
|
let combined = await this.memoryService.getCombinedMemoryContent(
|
|
@@ -195,17 +216,41 @@ export class MessageManager {
|
|
|
195
216
|
);
|
|
196
217
|
|
|
197
218
|
if (this.memoryRuleManager) {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
if (activeRules.length > 0) {
|
|
219
|
+
const { unconditional } = this.memoryRuleManager.getActiveRulesSplit([]);
|
|
220
|
+
if (unconditional.length > 0) {
|
|
201
221
|
combined += "\n\n";
|
|
202
|
-
combined +=
|
|
222
|
+
combined += unconditional.map((r) => r.content).join("\n\n");
|
|
203
223
|
}
|
|
204
224
|
}
|
|
205
225
|
|
|
206
226
|
return combined;
|
|
207
227
|
}
|
|
208
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Get memory content for message-array injection:
|
|
231
|
+
* - prependContent: AGENTS.md + user memory + unconditional rules (for head injection)
|
|
232
|
+
* Conditional rules are handled separately via processTriggeredRules().
|
|
233
|
+
*/
|
|
234
|
+
public async getMemoryForInjection(): Promise<{
|
|
235
|
+
prependContent: string;
|
|
236
|
+
}> {
|
|
237
|
+
const baseMemory = await this.memoryService.getCombinedMemoryContent(
|
|
238
|
+
this.workdir,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
let prependContent = baseMemory;
|
|
242
|
+
|
|
243
|
+
if (this.memoryRuleManager) {
|
|
244
|
+
const { unconditional } = this.memoryRuleManager.getActiveRulesSplit([]);
|
|
245
|
+
if (unconditional.length > 0) {
|
|
246
|
+
prependContent += "\n\n";
|
|
247
|
+
prependContent += unconditional.map((r) => r.content).join("\n\n");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return { prependContent };
|
|
252
|
+
}
|
|
253
|
+
|
|
209
254
|
/**
|
|
210
255
|
* Compute the transcript path using cached encoded workdir
|
|
211
256
|
* Called during construction and when sessionId changes
|
|
@@ -243,62 +288,43 @@ export class MessageManager {
|
|
|
243
288
|
}
|
|
244
289
|
|
|
245
290
|
/**
|
|
246
|
-
*
|
|
291
|
+
* Record a file path read via the Read tool, to be matched against
|
|
292
|
+
* conditional memory rules before the next AI call.
|
|
247
293
|
*/
|
|
248
|
-
public
|
|
294
|
+
public triggerFileRead(filePath: string): void {
|
|
249
295
|
const normalizedPath = isAbsolute(filePath)
|
|
250
296
|
? relative(this.workdir, filePath)
|
|
251
297
|
: filePath;
|
|
252
|
-
this.
|
|
298
|
+
this.pendingFileReadTriggers.add(normalizedPath);
|
|
253
299
|
}
|
|
254
300
|
|
|
255
301
|
/**
|
|
256
|
-
*
|
|
302
|
+
* Check if a file has been read (for read-before-edit enforcement).
|
|
303
|
+
* Uses recentFileReads Map populated from Read tool execution.
|
|
257
304
|
*/
|
|
258
|
-
public
|
|
305
|
+
public hasFileBeenRead(filePath: string): boolean {
|
|
259
306
|
const normalizedPath = isAbsolute(filePath)
|
|
260
307
|
? relative(this.workdir, filePath)
|
|
261
308
|
: filePath;
|
|
262
|
-
return
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
* Extracts and adds file paths from a message's tool blocks.
|
|
267
|
-
*/
|
|
268
|
-
private addPathsFromMessage(message: Message): void {
|
|
269
|
-
for (const block of message.blocks) {
|
|
270
|
-
if (block.type === "tool" && block.parameters) {
|
|
271
|
-
try {
|
|
272
|
-
const params = JSON.parse(block.parameters) as Record<
|
|
273
|
-
string,
|
|
274
|
-
unknown
|
|
275
|
-
>;
|
|
276
|
-
const paths = this.extractPathsFromParams(params);
|
|
277
|
-
for (const p of paths) {
|
|
278
|
-
this.touchFile(p);
|
|
279
|
-
}
|
|
280
|
-
} catch {
|
|
281
|
-
// Ignore parse errors
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
309
|
+
return (
|
|
310
|
+
this.recentFileReads.has(normalizedPath) ||
|
|
311
|
+
this.recentFileReads.has(filePath)
|
|
312
|
+
);
|
|
285
313
|
}
|
|
286
314
|
|
|
287
315
|
public setMessages(messages: Message[]): void {
|
|
288
316
|
const oldLength = this.messages.length;
|
|
289
317
|
this.messages = [...messages];
|
|
290
318
|
|
|
291
|
-
// Incrementally
|
|
319
|
+
// Incrementally extract metadata from new messages
|
|
292
320
|
const newMessages = messages.slice(oldLength);
|
|
293
321
|
for (const message of newMessages) {
|
|
294
|
-
this.addPathsFromMessage(message);
|
|
295
322
|
this.extractFileReadsFromMessage(message);
|
|
296
323
|
this.extractSkillInvocationsFromMessage(message);
|
|
297
324
|
}
|
|
298
325
|
|
|
299
326
|
// Also check if the last message was updated (common for tool blocks)
|
|
300
327
|
if (messages.length > 0 && messages.length === oldLength) {
|
|
301
|
-
this.addPathsFromMessage(messages[messages.length - 1]);
|
|
302
328
|
this.extractFileReadsFromMessage(messages[messages.length - 1]);
|
|
303
329
|
this.extractSkillInvocationsFromMessage(messages[messages.length - 1]);
|
|
304
330
|
}
|
|
@@ -377,6 +403,9 @@ export class MessageManager {
|
|
|
377
403
|
this.setMessages([...sessionData.messages]);
|
|
378
404
|
this.setlatestTotalTokens(sessionData.metadata.latestTotalTokens);
|
|
379
405
|
|
|
406
|
+
// Rebuild loadedRuleIds from persisted meta messages (session restore)
|
|
407
|
+
this.rebuildLoadedRuleIds();
|
|
408
|
+
|
|
380
409
|
// Set saved message count to the number of loaded messages since they're already saved
|
|
381
410
|
// This must be done after setSessionId which resets it to 0
|
|
382
411
|
this.savedMessageCount = sessionData.messages.length;
|
|
@@ -568,18 +597,9 @@ export class MessageManager {
|
|
|
568
597
|
// Set new message list
|
|
569
598
|
this.setMessages(newMessages);
|
|
570
599
|
|
|
571
|
-
//
|
|
572
|
-
this.
|
|
573
|
-
|
|
574
|
-
this.addPathsFromMessage(message);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Scan compactedContent for file mentions
|
|
578
|
-
const fileMentionRegex = /(?:^|\s)@([\w.\-/]+)/g;
|
|
579
|
-
let match;
|
|
580
|
-
while ((match = fileMentionRegex.exec(compactedContent)) !== null) {
|
|
581
|
-
this.touchFile(match[1]);
|
|
582
|
-
}
|
|
600
|
+
// Clear and rebuild loaded rule IDs from remaining meta messages
|
|
601
|
+
this.clearLoadedRuleIds();
|
|
602
|
+
this.rebuildLoadedRuleIds();
|
|
583
603
|
|
|
584
604
|
// Trigger compaction callback
|
|
585
605
|
this.callbacks.onCompactBlockAdded?.(compactedContent);
|
|
@@ -1008,35 +1028,6 @@ export class MessageManager {
|
|
|
1008
1028
|
}
|
|
1009
1029
|
}
|
|
1010
1030
|
|
|
1011
|
-
private extractPathsFromParams(params: Record<string, unknown>): string[] {
|
|
1012
|
-
const paths: string[] = [];
|
|
1013
|
-
if (typeof params !== "object" || params === null) return paths;
|
|
1014
|
-
|
|
1015
|
-
// Common parameter names for file paths
|
|
1016
|
-
const pathKeys = [
|
|
1017
|
-
"path",
|
|
1018
|
-
"filePath",
|
|
1019
|
-
"file_path",
|
|
1020
|
-
"target_file",
|
|
1021
|
-
"targetFile",
|
|
1022
|
-
];
|
|
1023
|
-
for (const key of pathKeys) {
|
|
1024
|
-
if (typeof params[key] === "string") {
|
|
1025
|
-
paths.push(params[key]);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// Handle arrays of paths (e.g. in Glob or Grep results if we ever track those,
|
|
1030
|
-
// but here we track inputs to tools)
|
|
1031
|
-
if (Array.isArray(params.files)) {
|
|
1032
|
-
for (const f of params.files) {
|
|
1033
|
-
if (typeof f === "string") paths.push(f);
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
return paths;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
1031
|
/**
|
|
1041
1032
|
* Extract file read contents from tool result blocks in a message.
|
|
1042
1033
|
*/
|
|
@@ -1044,7 +1035,7 @@ export class MessageManager {
|
|
|
1044
1035
|
for (const block of message.blocks) {
|
|
1045
1036
|
if (
|
|
1046
1037
|
block.type === "tool" &&
|
|
1047
|
-
block.name ===
|
|
1038
|
+
block.name === READ_TOOL_NAME &&
|
|
1048
1039
|
block.stage === "end" &&
|
|
1049
1040
|
block.result &&
|
|
1050
1041
|
block.parameters
|
|
@@ -1143,4 +1134,30 @@ export class MessageManager {
|
|
|
1143
1134
|
public clearInvokedSkills(): void {
|
|
1144
1135
|
this.invokedSkills.clear();
|
|
1145
1136
|
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Clear the loaded rule IDs set (e.g., after compaction resets context).
|
|
1140
|
+
*/
|
|
1141
|
+
public clearLoadedRuleIds(): void {
|
|
1142
|
+
this.loadedRuleIds.clear();
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Rebuild loadedRuleIds from persisted messages (session restore / post-compaction).
|
|
1147
|
+
* Scans for <!-- rule: {ruleId} --> markers in user meta message content.
|
|
1148
|
+
*/
|
|
1149
|
+
public rebuildLoadedRuleIds(): void {
|
|
1150
|
+
for (const message of this.messages) {
|
|
1151
|
+
if (message.role !== "user" || !message.isMeta) continue;
|
|
1152
|
+
for (const block of message.blocks) {
|
|
1153
|
+
if (block.type === "text") {
|
|
1154
|
+
const content = (block as { content: string }).content;
|
|
1155
|
+
const match = content.match(/<!-- rule: (.+?) -->/);
|
|
1156
|
+
if (match) {
|
|
1157
|
+
this.loadedRuleIds.add(match[1]);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1146
1163
|
}
|
package/src/prompts/index.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { ToolPlugin } from "../tools/types.js";
|
|
|
3
3
|
import { isGitRepository } from "../utils/gitUtils.js";
|
|
4
4
|
import { getCurrentWorktreeSession } from "../utils/worktreeSession.js";
|
|
5
5
|
import { buildAutoMemoryPrompt } from "./autoMemory.js";
|
|
6
|
-
import { PermissionMode } from "../types/permissions.js";
|
|
7
6
|
import {
|
|
8
7
|
EXPLORE_SUBAGENT_TYPE,
|
|
9
8
|
PLAN_SUBAGENT_TYPE,
|
|
@@ -96,12 +95,12 @@ export function buildPlanModePrompt(
|
|
|
96
95
|
: `No plan file exists yet. You should create your plan at ${planFilePath} using the ${WRITE_TOOL_NAME} tool if you need to.`;
|
|
97
96
|
|
|
98
97
|
if (isSubagent) {
|
|
99
|
-
return `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
|
|
98
|
+
return `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 tasks). Instead, you should:
|
|
100
99
|
|
|
101
100
|
## Plan File Info:
|
|
102
101
|
${planFileInfo}
|
|
103
102
|
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.
|
|
104
|
-
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
|
|
103
|
+
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 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.`;
|
|
105
104
|
}
|
|
106
105
|
|
|
107
106
|
return `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 changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
|
|
@@ -154,14 +153,14 @@ In the agent prompt:
|
|
|
154
153
|
### Phase 3: Review
|
|
155
154
|
Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions.
|
|
156
155
|
1. Read the critical files identified by agents to deepen your understanding
|
|
157
|
-
2. Ensure
|
|
156
|
+
2. Ensure the plans align with the user's original request
|
|
158
157
|
3. Use ${ASK_USER_QUESTION_TOOL_NAME} to clarify any remaining questions with the user
|
|
159
158
|
|
|
160
159
|
### Phase 4: Final Plan
|
|
161
160
|
Goal: Write your final plan to the plan file (the only file you can edit).
|
|
162
161
|
- 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
|
|
163
162
|
- Include only your recommended approach, not all alternatives
|
|
164
|
-
- Ensure
|
|
163
|
+
- Ensure the the plan file is concise enough to scan quickly, but detailed enough to execute effectively
|
|
165
164
|
- Include the paths of critical files to be modified
|
|
166
165
|
- Reference existing functions and utilities you found that should be reused, with their file paths
|
|
167
166
|
- Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests)
|
|
@@ -177,6 +176,16 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|
|
177
176
|
|
|
178
177
|
export const DEFAULT_SYSTEM_PROMPT = BASE_SYSTEM_PROMPT;
|
|
179
178
|
|
|
179
|
+
/**
|
|
180
|
+
* A block of the system prompt with cacheability metadata.
|
|
181
|
+
* Static blocks (cacheable: true) get cache_control markers for Claude models.
|
|
182
|
+
* Dynamic blocks (cacheable: false) change per-turn and must not invalidate the cache.
|
|
183
|
+
*/
|
|
184
|
+
export interface SystemPromptBlock {
|
|
185
|
+
text: string;
|
|
186
|
+
cacheable: boolean;
|
|
187
|
+
}
|
|
188
|
+
|
|
180
189
|
export const COMPACT_MESSAGES_SYSTEM_PROMPT = `You are continuing work on a software engineering task. Write a detailed continuation summary that will allow you (or another instance of yourself) to resume work efficiently in a future context window where the conversation history will be replaced with this summary.
|
|
181
190
|
|
|
182
191
|
First, write your analysis in <analysis> tags as a thinking scratchpad:
|
|
@@ -237,33 +246,33 @@ export function buildSystemPrompt(
|
|
|
237
246
|
options: {
|
|
238
247
|
workdir?: string;
|
|
239
248
|
originalWorkdir?: string;
|
|
240
|
-
memory?: string;
|
|
241
249
|
language?: string;
|
|
242
250
|
isSubagent?: boolean;
|
|
243
251
|
autoMemory?: {
|
|
244
252
|
directory: string;
|
|
245
253
|
content: string;
|
|
246
254
|
};
|
|
247
|
-
permissionMode?: PermissionMode;
|
|
248
255
|
} = {},
|
|
249
|
-
):
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
256
|
+
): SystemPromptBlock[] {
|
|
257
|
+
// --- Static block (cacheable) ---
|
|
258
|
+
let staticText = basePrompt || DEFAULT_SYSTEM_PROMPT;
|
|
259
|
+
staticText += `\n\n${DOING_TASKS_PROMPT}`;
|
|
260
|
+
staticText += `\n\n${EXECUTING_ACTIONS_PROMPT}`;
|
|
253
261
|
|
|
254
262
|
if (tools.length > 0) {
|
|
255
|
-
|
|
263
|
+
staticText += `\n\n${TOOL_POLICY}`;
|
|
256
264
|
}
|
|
257
265
|
|
|
258
|
-
|
|
259
|
-
|
|
266
|
+
staticText += `\n\n${OUTPUT_EFFICIENCY_PROMPT}`;
|
|
267
|
+
staticText += `\n\n${TONE_AND_STYLE_PROMPT}`;
|
|
260
268
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
269
|
+
const blocks: SystemPromptBlock[] = [{ text: staticText, cacheable: true }];
|
|
270
|
+
|
|
271
|
+
// --- Dynamic block (not cacheable) ---
|
|
272
|
+
let dynamicText = "";
|
|
264
273
|
|
|
265
274
|
if (options.language) {
|
|
266
|
-
|
|
275
|
+
dynamicText += `\n\n# Language\nAlways respond in ${options.language}. Use ${options.language} for all explanations, comments, and communications with the user. Technical terms and code identifiers should remain in their original form.`;
|
|
267
276
|
}
|
|
268
277
|
|
|
269
278
|
if (options.workdir) {
|
|
@@ -280,7 +289,7 @@ export function buildSystemPrompt(
|
|
|
280
289
|
|
|
281
290
|
const worktreeSession = getCurrentWorktreeSession();
|
|
282
291
|
|
|
283
|
-
|
|
292
|
+
dynamicText += `
|
|
284
293
|
|
|
285
294
|
Here is useful information about the environment you are running in:
|
|
286
295
|
<env>
|
|
@@ -295,17 +304,17 @@ Today's date: ${today}
|
|
|
295
304
|
}
|
|
296
305
|
|
|
297
306
|
if (options.autoMemory) {
|
|
298
|
-
|
|
307
|
+
dynamicText += `\n\n${buildAutoMemoryPrompt(options.autoMemory.directory)}`;
|
|
299
308
|
if (options.autoMemory.content.trim()) {
|
|
300
|
-
|
|
309
|
+
dynamicText += `\n\n## MEMORY.md\n\n${options.autoMemory.content}`;
|
|
301
310
|
}
|
|
302
311
|
}
|
|
303
312
|
|
|
304
|
-
if (
|
|
305
|
-
|
|
313
|
+
if (dynamicText.trim()) {
|
|
314
|
+
blocks.push({ text: dynamicText, cacheable: false });
|
|
306
315
|
}
|
|
307
316
|
|
|
308
|
-
return
|
|
317
|
+
return blocks;
|
|
309
318
|
}
|
|
310
319
|
|
|
311
320
|
export function enhanceSystemPromptWithEnvDetails(
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
supportsPromptCaching,
|
|
17
17
|
extendUsageWithCacheMetrics,
|
|
18
18
|
type ClaudeUsage,
|
|
19
|
+
type ClaudeChatCompletionContentPartText,
|
|
19
20
|
} from "../utils/cacheControlUtils.js";
|
|
20
21
|
|
|
21
22
|
import * as os from "os";
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
COMPACT_MESSAGES_SYSTEM_PROMPT,
|
|
27
28
|
WEB_CONTENT_SYSTEM_PROMPT,
|
|
28
29
|
BTW_SYSTEM_PROMPT,
|
|
30
|
+
type SystemPromptBlock,
|
|
29
31
|
} from "../prompts/index.js";
|
|
30
32
|
import { GOAL_EVALUATION_SYSTEM_PROMPT } from "../constants/goalPrompts.js";
|
|
31
33
|
|
|
@@ -159,7 +161,7 @@ export interface CallAgentOptions {
|
|
|
159
161
|
workdir: string; // Current working directory
|
|
160
162
|
tools?: ChatCompletionFunctionTool[]; // Tool configuration
|
|
161
163
|
model?: string; // Custom model
|
|
162
|
-
systemPrompt?: string; // Custom system prompt
|
|
164
|
+
systemPrompt?: string | SystemPromptBlock[]; // Custom system prompt (string or structured blocks)
|
|
163
165
|
maxTokens?: number; // Maximum output tokens
|
|
164
166
|
toolChoice?:
|
|
165
167
|
| "auto"
|
|
@@ -262,22 +264,47 @@ export async function callAgent(
|
|
|
262
264
|
fetch: gatewayConfig.fetch,
|
|
263
265
|
});
|
|
264
266
|
|
|
265
|
-
//
|
|
266
|
-
const
|
|
267
|
+
// Determine model early (needed for system prompt construction)
|
|
268
|
+
const currentModel = model || modelConfig.model;
|
|
269
|
+
const resolvedMaxTokens = options.maxTokens ?? modelConfig.maxTokens;
|
|
267
270
|
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
// Build system message content
|
|
272
|
+
let systemMessage: ChatCompletionMessageParam;
|
|
273
|
+
if (Array.isArray(systemPrompt)) {
|
|
274
|
+
if (supportsPromptCaching(currentModel)) {
|
|
275
|
+
// For Claude models, map blocks to content parts with cache_control on cacheable blocks
|
|
276
|
+
const contentParts: ClaudeChatCompletionContentPartText[] =
|
|
277
|
+
systemPrompt.map((block) => {
|
|
278
|
+
const part: ClaudeChatCompletionContentPartText = {
|
|
279
|
+
type: "text",
|
|
280
|
+
text: block.text,
|
|
281
|
+
};
|
|
282
|
+
if (block.cacheable) {
|
|
283
|
+
part.cache_control = { type: "ephemeral" };
|
|
284
|
+
}
|
|
285
|
+
return part;
|
|
286
|
+
});
|
|
287
|
+
systemMessage = {
|
|
288
|
+
role: "system",
|
|
289
|
+
content: contentParts,
|
|
290
|
+
} as ChatCompletionMessageParam;
|
|
291
|
+
} else {
|
|
292
|
+
// For non-Claude models, join blocks into a single string
|
|
293
|
+
systemMessage = {
|
|
294
|
+
role: "system",
|
|
295
|
+
content: systemPrompt.map((b) => b.text).join("\n\n"),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
systemMessage = {
|
|
300
|
+
role: "system",
|
|
301
|
+
content: systemPrompt || "",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
273
304
|
|
|
274
305
|
// ChatCompletionMessageParam[] is already in OpenAI format, add system prompt to the beginning
|
|
275
306
|
openaiMessages = [systemMessage, ...messages];
|
|
276
307
|
|
|
277
|
-
// Apply cache control for Claude models
|
|
278
|
-
const currentModel = model || modelConfig.model;
|
|
279
|
-
const resolvedMaxTokens = options.maxTokens ?? modelConfig.maxTokens;
|
|
280
|
-
|
|
281
308
|
processedTools = tools;
|
|
282
309
|
|
|
283
310
|
if (supportsPromptCaching(currentModel)) {
|