1use crate::memory::memdir_paths::{
7 ensure_memory_dir_exists, get_auto_mem_entrypoint, get_auto_mem_path, is_auto_memory_enabled,
8};
9use crate::memory::types::EntrypointTruncation;
10
11pub const ENTRYPOINT_NAME: &str = "MEMORY.md";
13
14pub const MAX_ENTRYPOINT_LINES: usize = 200;
16
17pub const MAX_ENTRYPOINT_BYTES: usize = 25_000;
19
20pub const DIR_EXISTS_GUIDANCE: &str =
24 "This directory already exists — write to it directly with the Write tool (do not run mkdir or check for its existence).";
25
26pub fn truncate_entrypoint_content(raw: &str) -> EntrypointTruncation {
30 let trimmed = raw.trim();
31 let content_lines: Vec<&str> = trimmed.lines().collect();
32 let line_count = content_lines.len();
33 let byte_count = trimmed.len();
34
35 let was_line_truncated = line_count > MAX_ENTRYPOINT_LINES;
36 let was_byte_truncated = byte_count > MAX_ENTRYPOINT_BYTES;
39
40 if !was_line_truncated && !was_byte_truncated {
41 return EntrypointTruncation {
42 content: trimmed.to_string(),
43 line_count,
44 byte_count,
45 was_line_truncated,
46 was_byte_truncated,
47 };
48 }
49
50 let truncated = if was_line_truncated {
51 content_lines[..MAX_ENTRYPOINT_LINES].join("\n")
52 } else {
53 trimmed.to_string()
54 };
55
56 let truncated = if truncated.len() > MAX_ENTRYPOINT_BYTES {
57 if let Some(cut_at) = truncated.rfind('\n') {
58 if cut_at > 0 {
59 truncated[..cut_at].to_string()
60 } else {
61 truncated[..MAX_ENTRYPOINT_BYTES].to_string()
62 }
63 } else {
64 truncated[..MAX_ENTRYPOINT_BYTES].to_string()
65 }
66 } else {
67 truncated
68 };
69
70 let reason = if was_byte_truncated && !was_line_truncated {
71 format!(
72 "{} (limit: {} bytes) — index entries are too long",
73 format_file_size(byte_count),
74 format_file_size(MAX_ENTRYPOINT_BYTES)
75 )
76 } else if was_line_truncated && !was_byte_truncated {
77 format!("{} lines (limit: {})", line_count, MAX_ENTRYPOINT_LINES)
78 } else {
79 format!("{} lines and {}", line_count, format_file_size(byte_count))
80 };
81
82 let content = format!(
83 "{}\n\n> WARNING: {} is {}. Only part of it was loaded. Keep index entries to one line under ~200 chars; move detail into topic files.",
84 truncated, ENTRYPOINT_NAME, reason
85 );
86
87 EntrypointTruncation {
88 content,
89 line_count,
90 byte_count,
91 was_line_truncated,
92 was_byte_truncated,
93 }
94}
95
96fn format_file_size(bytes: usize) -> String {
98 if bytes >= 1_000_000 {
99 format!("{:.1}M", bytes as f64 / 1_000_000.0)
100 } else if bytes >= 1_000 {
101 format!("{:.1}K", bytes as f64 / 1_000.0)
102 } else {
103 format!("{}B", bytes)
104 }
105}
106
107pub fn build_memory_lines(
112 display_name: &str,
113 memory_dir: &str,
114 extra_guidelines: Option<&[&str]>,
115 skip_index: bool,
116) -> Vec<String> {
117 let how_to_save = if skip_index {
118 vec![
119 "## How to save memories".to_string(),
120 String::new(),
121 "Write each memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:".to_string(),
122 String::new(),
123 ]
124 .into_iter()
125 .chain(MEMORY_FRONTMATTER_EXAMPLE.iter().map(|s| s.to_string()))
126 .chain(vec![
127 String::new(),
128 "- Keep the name, description, and type fields in memory files up-to-date with the content".to_string(),
129 "- Organize memory semantically by topic, not chronologically".to_string(),
130 "- Update or remove memories that turn out to be wrong or outdated".to_string(),
131 "- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.".to_string(),
132 ])
133 .collect::<Vec<_>>()
134 } else {
135 vec![
136 "## How to save memories".to_string(),
137 String::new(),
138 "Saving a memory is a two-step process:".to_string(),
139 String::new(),
140 "**Step 1** — write the memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:".to_string(),
141 String::new(),
142 ]
143 .into_iter()
144 .chain(MEMORY_FRONTMATTER_EXAMPLE.iter().map(|s| s.to_string()))
145 .chain(vec — one-line hook`. It has no frontmatter. Never write memory content directly into `{}`.", ENTRYPOINT_NAME, ENTRYPOINT_NAME, ENTRYPOINT_NAME),
148 String::new(),
149 format!("- `{}` is always loaded into your conversation context — lines after {} will be truncated, so keep the index concise", ENTRYPOINT_NAME, MAX_ENTRYPOINT_LINES),
150 "- Keep the name, description, and type fields in memory files up-to-date with the content".to_string(),
151 "- Organize memory semantically by topic, not chronologically".to_string(),
152 "- Update or remove memories that turn out to be wrong or outdated".to_string(),
153 "- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.".to_string(),
154 ])
155 .collect::<Vec<_>>()
156 };
157
158 let mut lines = vec![
159 format!("# {}", display_name),
160 String::new(),
161 format!(
162 "You have a persistent, file-based memory system at `{}`. {}",
163 memory_dir, DIR_EXISTS_GUIDANCE
164 ),
165 String::new(),
166 "You should build up this memory system over time so that future conversations can have a complete picture of who the user is, how they'd like to collaborate with you, what behaviors to avoid or repeat, and the context behind the work the user gives you.".to_string(),
167 String::new(),
168 "If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.".to_string(),
169 String::new(),
170 ];
171
172 lines.extend(TYPES_SECTION_INDIVIDUAL.iter().map(|s| s.to_string()));
174 lines.push(String::new());
175
176 lines.extend(WHAT_NOT_TO_SAVE_SECTION.iter().map(|s| s.to_string()));
178 lines.push(String::new());
179
180 lines.extend(how_to_save);
182 lines.push(String::new());
183
184 lines.extend(WHEN_TO_ACCESS_SECTION.iter().map(|s| s.to_string()));
186 lines.push(String::new());
187
188 lines.extend(TRUSTING_RECALL_SECTION.iter().map(|s| s.to_string()));
190 lines.push(String::new());
191
192 lines.push("## Memory and other forms of persistence".to_string());
194 lines.push("Memory is one of several persistence mechanisms available to you as you assist the user in a given conversation. The distinction is often that memory can be recalled in future conversations and should not be used for persisting information that is only useful within the scope of the current conversation.".to_string());
195 lines.push("- When to use or update a plan instead of memory: If you are about to start a non-trivial implementation task and would like to reach alignment with the user on your approach you should use a Plan rather than saving this information to memory. Similarly, if you already have a plan within the conversation and you have changed your approach persist that change by updating the plan rather than saving a memory.".to_string());
196 lines.push("- When to use or update tasks instead of memory: When you need to break your work in current conversation into discrete steps or keep track of your progress use tasks instead of saving to memory. Tasks are great for persisting information about the work that needs to be done in the current conversation, but memory should be reserved for information that will be useful in future conversations.".to_string());
197 lines.push(String::new());
198
199 if let Some(guidelines) = extra_guidelines {
201 lines.extend(guidelines.iter().map(|s| s.to_string()));
202 lines.push(String::new());
203 }
204
205 lines.extend(build_searching_past_context_section(memory_dir));
207
208 lines
209}
210
211const MEMORY_FRONTMATTER_EXAMPLE: &[&str] = &[
213 "```markdown",
214 "---",
215 "name: {{memory name}}",
216 "description: {{one-line description — used to decide relevance in future conversations, so be specific}}",
217 "type: {{user, feedback, project, reference}}",
218 "---",
219 "",
220 "{{memory content — for feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines}}",
221 "```",
222];
223
224const TYPES_SECTION_INDIVIDUAL: &[&str] = &[
226 "## Types of memory",
227 "",
228 "There are several discrete types of memory that you can store in your memory system:",
229 "",
230 "<types>",
231 "<type>",
232 " <name>user</name>",
233 " <description>Contain information about the user's role, goals, responsibilities, and knowledge. Great user memories help you tailor your future behavior to the user's preferences and perspective. Your goal in reading and writing these memories is to build up an understanding of who the user is and how you can be most helpful to them specifically.</description>",
234 " <when_to_save>When you learn any details about the user's role, preferences, responsibilities, or knowledge</when_to_save>",
235 " <how_to_use>When your work should be informed by the user's profile or perspective.</how_to_use>",
236 "</type>",
237 "<type>",
238 " <name>feedback</name>",
239 " <description>Guidance the user has given you about how to approach work — both what to avoid and what to keep doing.</description>",
240 " <when_to_save>Any time the user corrects your approach (\"no not that\", \"don't\", \"stop doing X\") OR confirms a non-obvious approach worked.</when_to_save>",
241 " <how_to_use>Let these memories guide your behavior so that the user does not need to offer the same guidance twice.</how_to_use>",
242 "</type>",
243 "<type>",
244 " <name>project</name>",
245 " <description>Information that you learn about ongoing work, goals, initiatives, bugs, or incidents within the project that is not otherwise derivable from the code or git history.</description>",
246 " <when_to_save>When you learn who is doing what, why, or by when.</when_to_save>",
247 " <how_to_use>Use these memories to more fully understand the details and nuance behind the user's request.</how_to_use>",
248 "</type>",
249 "<type>",
250 " <name>reference</name>",
251 " <description>Stores pointers to where information can be found in external systems.</description>",
252 " <when_to_save>When you learn about resources in external systems and their purpose.</when_to_save>",
253 " <how_to_use>When the user references an external system or information that may be in an external system.</how_to_use>",
254 "</type>",
255 "</types>",
256 "",
257];
258
259const WHAT_NOT_TO_SAVE_SECTION: &[&str] = &[
261 "## What NOT to save in memory",
262 "",
263 "- Code patterns, conventions, architecture, file paths, or project structure — these can be derived by reading the current project state.",
264 "- Git history, recent changes, or who-changed-what — `git log` / `git blame` are authoritative.",
265 "- Debugging solutions or fix recipes — the fix is in the code; the commit message has the context.",
266 "- Anything already documented in AI.md files.",
267 "- Ephemeral task details: in-progress work, temporary state, current conversation context.",
268 "",
269 "These exclusions apply even when the user explicitly asks you to save. If they ask you to save a PR list or activity summary, ask what was *surprising* or *non-obvious* about it — that is the part worth keeping.",
270];
271
272const WHEN_TO_ACCESS_SECTION: &[&str] = &[
274 "## When to access memories",
275 "- When memories seem relevant, or the user references prior-conversation work.",
276 "- You MUST access memory when the user explicitly asks you to check, recall, or remember.",
277 "- If the user says to *ignore* or *not use* memory: proceed as if MEMORY.md were empty.",
278 "- Memory records can become stale over time. Verify that the memory is still correct and up-to-date.",
279];
280
281const TRUSTING_RECALL_SECTION: &[&str] = &[
283 "## Before recommending from memory",
284 "",
285 "A memory that names a specific function, file, or flag is a claim that it existed *when the memory was written*. It may have been renamed, removed, or never merged. Before recommending it:",
286 "",
287 "- If the memory names a file path: check the file exists.",
288 "- If the memory names a function or flag: grep for it.",
289 "- If the user is about to act on your recommendation (not just asking about history), verify first.",
290 "",
291 "\"The memory says X exists\" is not the same as \"X exists now.\"",
292 "",
293 "A memory that summarizes repo state (activity logs, architecture snapshots) is frozen in time. If the user asks about *recent* or *current* state, prefer `git log` or reading the code over recalling the snapshot.",
294];
295
296fn build_searching_past_context_section(auto_mem_dir: &str) -> Vec<String> {
298 vec![]
301}
302
303pub fn build_memory_prompt(params: BuildMemoryPromptParams) -> String {
306 let BuildMemoryPromptParams {
307 display_name,
308 extra_guidelines,
309 } = params;
310
311 let memory_dir = get_auto_mem_path();
312 let memory_dir_str = memory_dir.to_string_lossy();
313
314 let entrypoint_path = get_auto_mem_entrypoint();
316 let entrypoint_content = if entrypoint_path.exists() {
317 std::fs::read_to_string(&entrypoint_path).unwrap_or_default()
318 } else {
319 String::new()
320 };
321
322 let mut lines = build_memory_lines(
323 &display_name,
324 &memory_dir_str,
325 extra_guidelines.as_deref(),
326 false,
327 );
328
329 if !entrypoint_content.trim().is_empty() {
330 let t = truncate_entrypoint_content(&entrypoint_content);
331 lines.push(format!("## {}", ENTRYPOINT_NAME));
332 lines.push(String::new());
333 lines.push(t.content);
334 } else {
335 lines.push(format!("## {}", ENTRYPOINT_NAME));
336 lines.push(String::new());
337 lines.push(format!(
338 "Your {} is currently empty. When you save new memories, they will appear here.",
339 ENTRYPOINT_NAME
340 ));
341 }
342
343 lines.join("\n")
344}
345
346pub struct BuildMemoryPromptParams<'a> {
348 pub display_name: &'a str,
349 pub extra_guidelines: Option<Vec<&'a str>>,
350}
351
352pub async fn load_memory_prompt() -> Option<String> {
355 if !is_auto_memory_enabled() {
356 return None;
357 }
358
359 let auto_dir = get_auto_mem_path();
360 ensure_memory_dir_exists(&auto_dir).ok()?;
362
363 Some(build_memory_prompt(BuildMemoryPromptParams {
364 display_name: "auto memory",
365 extra_guidelines: None,
366 }))
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn test_truncate_entrypoint_content_no_truncation() {
375 let content = "line 1\nline 2\nline 3";
376 let result = truncate_entrypoint_content(content);
377
378 assert_eq!(result.content, content);
379 assert!(!result.was_line_truncated);
380 assert!(!result.was_byte_truncated);
381 }
382
383 #[test]
384 fn test_truncate_entrypoint_content_line_truncation() {
385 let content: String = (0..=MAX_ENTRYPOINT_LINES)
386 .map(|i| format!("line {}\n", i))
387 .collect();
388 let result = truncate_entrypoint_content(&content);
389
390 assert!(result.was_line_truncated);
391 assert!(result.content.contains("WARNING: MEMORY.md is"));
392 }
393
394 #[test]
395 fn test_build_memory_lines() {
396 let lines = build_memory_lines("auto memory", "/tmp/memory", None, false);
397 assert!(!lines.is_empty());
398 assert!(lines.iter().any(|l| l.contains("Types of memory")));
399 assert!(lines.iter().any(|l| l.contains("How to save memories")));
400 }
401
402 #[test]
403 fn test_build_memory_lines_skip_index() {
404 let lines = build_memory_lines("auto memory", "/tmp/memory", None, true);
405 assert!(!lines.iter().any(|l| l.contains("Step 1")));
406 assert!(!lines.iter().any(|l| l.contains("Step 2")));
407 }
408
409 #[test]
410 fn test_build_memory_prompt() {
411 let prompt = build_memory_prompt(BuildMemoryPromptParams {
412 display_name: "auto memory",
413 extra_guidelines: None,
414 });
415 assert!(prompt.contains("auto memory"));
416 assert!(prompt.contains("MEMORY.md"));
417 }
418}