wave-agent-sdk 0.18.3 → 0.18.4
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/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/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/editTool.js +3 -3
- package/dist/tools/readTool.js +2 -2
- package/dist/tools/writeTool.js +2 -2
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +1 -0
- package/dist/utils/openaiClient.d.ts.map +1 -1
- package/dist/utils/openaiClient.js +28 -7
- 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/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/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/editTool.ts +3 -3
- package/src/tools/readTool.ts +2 -2
- package/src/tools/writeTool.ts +2 -2
- package/src/utils/containerSetup.ts +1 -0
- package/src/utils/openaiClient.ts +35 -7
- package/src/utils/taskReminder.ts +7 -19
- 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
|
@@ -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)) {
|
package/src/services/memory.ts
CHANGED
|
@@ -8,24 +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 =
|
|
11
|
+
private _cachedProjectMemory: string | null = null;
|
|
12
|
+
private _cachedUserMemory: string | null = null;
|
|
13
13
|
private _cachedCombinedMemory: string | null = null;
|
|
14
|
+
private _cachedAutoMemoryContent: string | null = null;
|
|
14
15
|
|
|
15
16
|
constructor(private container: Container) {}
|
|
16
17
|
|
|
17
18
|
public get cachedProjectMemory(): string {
|
|
18
|
-
return this._cachedProjectMemory;
|
|
19
|
+
return this._cachedProjectMemory ?? "";
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
public get cachedUserMemory(): string {
|
|
22
|
-
return this._cachedUserMemory;
|
|
23
|
+
return this._cachedUserMemory ?? "";
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
public clearCache(): void {
|
|
26
|
-
this._cachedProjectMemory =
|
|
27
|
-
this._cachedUserMemory =
|
|
27
|
+
this._cachedProjectMemory = null;
|
|
28
|
+
this._cachedUserMemory = null;
|
|
28
29
|
this._cachedCombinedMemory = null;
|
|
30
|
+
this._cachedAutoMemoryContent = null;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -76,15 +78,20 @@ export class MemoryService {
|
|
|
76
78
|
* Get the first 200 lines of MEMORY.md.
|
|
77
79
|
*/
|
|
78
80
|
async getAutoMemoryContent(workdir: string): Promise<string> {
|
|
81
|
+
if (this._cachedAutoMemoryContent !== null) {
|
|
82
|
+
return this._cachedAutoMemoryContent;
|
|
83
|
+
}
|
|
79
84
|
const memoryDir = this.getAutoMemoryDirectory(workdir);
|
|
80
85
|
const memoryFile = path.join(memoryDir, "MEMORY.md");
|
|
81
86
|
|
|
82
87
|
try {
|
|
83
88
|
const content = await fs.readFile(memoryFile, "utf-8");
|
|
84
89
|
const lines = content.split("\n").slice(0, 200);
|
|
85
|
-
|
|
90
|
+
this._cachedAutoMemoryContent = lines.join("\n");
|
|
91
|
+
return this._cachedAutoMemoryContent;
|
|
86
92
|
} catch (error) {
|
|
87
93
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
94
|
+
this._cachedAutoMemoryContent = "";
|
|
88
95
|
return "";
|
|
89
96
|
}
|
|
90
97
|
logger.error("Failed to read auto-memory content:", error);
|
|
@@ -123,6 +130,9 @@ export class MemoryService {
|
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
async getUserMemoryContent(): Promise<string> {
|
|
133
|
+
if (this._cachedUserMemory !== null) {
|
|
134
|
+
return this._cachedUserMemory;
|
|
135
|
+
}
|
|
126
136
|
try {
|
|
127
137
|
await this.ensureUserMemoryFile();
|
|
128
138
|
const content = await fs.readFile(USER_MEMORY_FILE, "utf-8");
|
|
@@ -145,13 +155,15 @@ export class MemoryService {
|
|
|
145
155
|
contentLength: claudeContent.length,
|
|
146
156
|
},
|
|
147
157
|
);
|
|
148
|
-
|
|
158
|
+
this._cachedUserMemory = claudeContent;
|
|
159
|
+
return this._cachedUserMemory;
|
|
149
160
|
} catch {
|
|
150
161
|
// CLAUDE.md doesn't exist or can't be read, return the default template
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
|
|
154
|
-
|
|
165
|
+
this._cachedUserMemory = content;
|
|
166
|
+
return this._cachedUserMemory;
|
|
155
167
|
} catch (error) {
|
|
156
168
|
logger.error("Failed to read user memory:", error);
|
|
157
169
|
return "";
|
|
@@ -159,6 +171,9 @@ export class MemoryService {
|
|
|
159
171
|
}
|
|
160
172
|
|
|
161
173
|
async readMemoryFile(workdir: string): Promise<string> {
|
|
174
|
+
if (this._cachedProjectMemory !== null) {
|
|
175
|
+
return this._cachedProjectMemory;
|
|
176
|
+
}
|
|
162
177
|
const memoryFilePath = path.join(workdir, "AGENTS.md");
|
|
163
178
|
try {
|
|
164
179
|
const content = await fs.readFile(memoryFilePath, "utf-8");
|
|
@@ -166,7 +181,8 @@ export class MemoryService {
|
|
|
166
181
|
memoryFilePath,
|
|
167
182
|
contentLength: content.length,
|
|
168
183
|
});
|
|
169
|
-
|
|
184
|
+
this._cachedProjectMemory = content;
|
|
185
|
+
return this._cachedProjectMemory;
|
|
170
186
|
} catch (error) {
|
|
171
187
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
172
188
|
// Fallback to CLAUDE.md
|
|
@@ -177,12 +193,14 @@ export class MemoryService {
|
|
|
177
193
|
memoryFilePath: claudeMemoryPath,
|
|
178
194
|
contentLength: content.length,
|
|
179
195
|
});
|
|
180
|
-
|
|
196
|
+
this._cachedProjectMemory = content;
|
|
197
|
+
return this._cachedProjectMemory;
|
|
181
198
|
} catch (claudeError) {
|
|
182
199
|
if ((claudeError as NodeJS.ErrnoException).code === "ENOENT") {
|
|
183
200
|
logger.debug("Neither AGENTS.md nor CLAUDE.md found", {
|
|
184
201
|
memoryFilePath,
|
|
185
202
|
});
|
|
203
|
+
this._cachedProjectMemory = "";
|
|
186
204
|
return "";
|
|
187
205
|
}
|
|
188
206
|
logger.error("Failed to read CLAUDE.md fallback", {
|
|
@@ -201,14 +219,14 @@ export class MemoryService {
|
|
|
201
219
|
if (this._cachedCombinedMemory !== null) {
|
|
202
220
|
return this._cachedCombinedMemory;
|
|
203
221
|
}
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
const projectMemory = await this.readMemoryFile(workdir);
|
|
223
|
+
const userMemory = await this.getUserMemoryContent();
|
|
206
224
|
|
|
207
225
|
let combined = "";
|
|
208
|
-
if (
|
|
209
|
-
if (
|
|
226
|
+
if (projectMemory.trim()) combined += projectMemory;
|
|
227
|
+
if (userMemory.trim()) {
|
|
210
228
|
if (combined) combined += "\n\n";
|
|
211
|
-
combined +=
|
|
229
|
+
combined += userMemory;
|
|
212
230
|
}
|
|
213
231
|
this._cachedCombinedMemory = combined;
|
|
214
232
|
return combined;
|
package/src/tools/agentTool.ts
CHANGED
|
@@ -266,10 +266,32 @@ When using the Agent tool, you must specify a subagent_type parameter to select
|
|
|
266
266
|
});
|
|
267
267
|
} catch (error) {
|
|
268
268
|
if (!isBackgrounded) {
|
|
269
|
+
// Extract content from the subagent's last assistant message.
|
|
270
|
+
// aiManager.sendAIMessage() catch block already added an error
|
|
271
|
+
// block via addErrorBlock(), so the error info is in the message
|
|
272
|
+
// history — just return it directly.
|
|
273
|
+
let content = "";
|
|
274
|
+
try {
|
|
275
|
+
const messages = instance.messageManager.getMessages();
|
|
276
|
+
const lastAssistant = messages
|
|
277
|
+
.filter((m) => m.role === "assistant")
|
|
278
|
+
.pop();
|
|
279
|
+
if (lastAssistant) {
|
|
280
|
+
content = lastAssistant.blocks
|
|
281
|
+
.filter((b) => b.type === "text" || b.type === "error")
|
|
282
|
+
.map((b) => (b as { content: string }).content)
|
|
283
|
+
.join("\n")
|
|
284
|
+
.trim();
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// Ignore errors when extracting messages
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const errorMsg = `Agent delegation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
269
291
|
resolve({
|
|
270
292
|
success: false,
|
|
271
|
-
content:
|
|
272
|
-
error:
|
|
293
|
+
content: content || errorMsg,
|
|
294
|
+
error: errorMsg,
|
|
273
295
|
shortResult: "Delegation error",
|
|
274
296
|
});
|
|
275
297
|
}
|