Skip to content

Commit 7b7a3b5

Browse files
committed
feat(session): implement Phase 2 — system prompt sections + memory system
Implement all 13 todo!() items across 6 files: - sections.rs: register 8 default section builders (env, tools, crab_md, memory, git, skills, tips, custom) with static/dynamic cache scope separation - memory_types.rs: YAML frontmatter parser, body extractor, prompt formatter, MemoryType FromStr impl - memory_relevance.rs: directory scanner, keyword-overlap scoring with type priority and name match, budget-constrained selection - memory_age.rs: exponential decay scoring (30-day half-life), human-readable age text, staleness caveats, pruning suggestions - memory_extract.rs: pattern-based extraction from conversations (feedback corrections, reference URLs, project facts), interval throttle logic - team_memory.rs: team memory directory resolution, load/save with YAML frontmatter serialization, filename slugification 70+ new tests added across all files.
1 parent 3e126a1 commit 7b7a3b5

6 files changed

Lines changed: 1177 additions & 91 deletions

File tree

crates/agent/src/system_prompt/sections.rs

Lines changed: 261 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,29 @@ impl SectionRegistry {
7878
}
7979

8080
/// Create a registry with all default section builders.
81+
///
82+
/// Registers sections in order:
83+
/// 1. `env` (Static) — environment info (OS, cwd, date, model)
84+
/// 2. `tools` (Static) — available tool descriptions
85+
/// 3. `crab_md` (Static) — CRAB.md project instructions
86+
/// 4. `memory` (Static) — auto-memory content
87+
/// 5. `git` (Dynamic) — current git status
88+
/// 6. `skills` (Static) — available skill descriptions (placeholder)
89+
/// 7. `tips` (Static) — contextual tips (placeholder)
90+
/// 8. `custom` (Dynamic) — user custom instructions
8191
pub fn default_sections() -> Self {
82-
todo!("Register: env, tools, memory, git, crab_md, skills, tips, custom")
92+
let mut registry = Self::new();
93+
94+
registry.register(Box::new(EnvSection));
95+
registry.register(Box::new(ToolsSection));
96+
registry.register(Box::new(CrabMdSection));
97+
registry.register(Box::new(MemorySection));
98+
registry.register(Box::new(GitSection));
99+
registry.register(Box::new(SkillsSection));
100+
registry.register(Box::new(TipsSection));
101+
registry.register(Box::new(CustomSection));
102+
103+
registry
83104
}
84105

85106
/// Build all sections and assemble them, inserting the dynamic boundary marker.
@@ -131,6 +152,160 @@ impl Default for SectionRegistry {
131152
}
132153
}
133154

155+
// ─── Default section builders ─────────────────────────────────────────
156+
157+
/// Environment section: OS, working directory, date, model info.
158+
struct EnvSection;
159+
160+
impl SectionBuilder for EnvSection {
161+
fn name(&self) -> &'static str {
162+
"env"
163+
}
164+
165+
fn cache_scope(&self) -> CacheScope {
166+
CacheScope::Static
167+
}
168+
169+
fn build(&self, ctx: &SectionContext) -> Option<String> {
170+
let mut s = String::with_capacity(256);
171+
s.push_str("# Environment\n");
172+
let _ = writeln!(s, "- Working directory: {}", ctx.project_dir.display());
173+
let _ = writeln!(s, "- Platform: {}", std::env::consts::OS);
174+
let _ = writeln!(s, "- Architecture: {}", std::env::consts::ARCH);
175+
Some(s)
176+
}
177+
}
178+
179+
/// Tools section: available tool descriptions.
180+
struct ToolsSection;
181+
182+
impl SectionBuilder for ToolsSection {
183+
fn name(&self) -> &'static str {
184+
"tools"
185+
}
186+
187+
fn cache_scope(&self) -> CacheScope {
188+
CacheScope::Static
189+
}
190+
191+
fn build(&self, ctx: &SectionContext) -> Option<String> {
192+
if ctx.tool_descriptions.is_empty() {
193+
return None;
194+
}
195+
Some(format!("# Available Tools\n{}", ctx.tool_descriptions))
196+
}
197+
}
198+
199+
/// CRAB.md section: project-level instructions.
200+
struct CrabMdSection;
201+
202+
impl SectionBuilder for CrabMdSection {
203+
fn name(&self) -> &'static str {
204+
"crab_md"
205+
}
206+
207+
fn cache_scope(&self) -> CacheScope {
208+
CacheScope::Static
209+
}
210+
211+
fn build(&self, ctx: &SectionContext) -> Option<String> {
212+
ctx.crab_md_content
213+
.map(|content| format!("# Project Instructions (CRAB.md)\n{content}"))
214+
}
215+
}
216+
217+
/// Memory section: auto-memory content from MEMORY.md and topic files.
218+
struct MemorySection;
219+
220+
impl SectionBuilder for MemorySection {
221+
fn name(&self) -> &'static str {
222+
"memory"
223+
}
224+
225+
fn cache_scope(&self) -> CacheScope {
226+
CacheScope::Static
227+
}
228+
229+
fn build(&self, ctx: &SectionContext) -> Option<String> {
230+
ctx.memory_content
231+
.map(|content| format!("# Memory\n{content}"))
232+
}
233+
}
234+
235+
/// Git section: current git status (dynamic — changes per turn).
236+
struct GitSection;
237+
238+
impl SectionBuilder for GitSection {
239+
fn name(&self) -> &'static str {
240+
"git"
241+
}
242+
243+
fn cache_scope(&self) -> CacheScope {
244+
CacheScope::Dynamic
245+
}
246+
247+
fn build(&self, ctx: &SectionContext) -> Option<String> {
248+
ctx.git_status
249+
.map(|status| format!("# Git Status\n{status}"))
250+
}
251+
}
252+
253+
/// Skills section: available skills (placeholder until skill system is built).
254+
struct SkillsSection;
255+
256+
impl SectionBuilder for SkillsSection {
257+
fn name(&self) -> &'static str {
258+
"skills"
259+
}
260+
261+
fn cache_scope(&self) -> CacheScope {
262+
CacheScope::Static
263+
}
264+
265+
fn build(&self, _ctx: &SectionContext) -> Option<String> {
266+
// Skills will be populated when the skill system (Phase 8) is built
267+
None
268+
}
269+
}
270+
271+
/// Tips section: contextual tips (placeholder until tips system is built).
272+
struct TipsSection;
273+
274+
impl SectionBuilder for TipsSection {
275+
fn name(&self) -> &'static str {
276+
"tips"
277+
}
278+
279+
fn cache_scope(&self) -> CacheScope {
280+
CacheScope::Static
281+
}
282+
283+
fn build(&self, _ctx: &SectionContext) -> Option<String> {
284+
// Tips will be populated when the tips system (Phase 11) is built
285+
None
286+
}
287+
}
288+
289+
/// Custom section: user-provided custom instructions (dynamic).
290+
struct CustomSection;
291+
292+
impl SectionBuilder for CustomSection {
293+
fn name(&self) -> &'static str {
294+
"custom"
295+
}
296+
297+
fn cache_scope(&self) -> CacheScope {
298+
CacheScope::Dynamic
299+
}
300+
301+
fn build(&self, ctx: &SectionContext) -> Option<String> {
302+
ctx.custom_instructions
303+
.map(|inst| format!("# Custom Instructions\n{inst}"))
304+
}
305+
}
306+
307+
// ─── Tests ─────────────────────────────────────────────────────────────
308+
134309
#[cfg(test)]
135310
mod tests {
136311
use super::*;
@@ -230,4 +405,89 @@ mod tests {
230405
assert_eq!(CacheScope::Static, CacheScope::Static);
231406
assert_ne!(CacheScope::Static, CacheScope::Dynamic);
232407
}
408+
409+
#[test]
410+
fn default_sections_creates_all_builders() {
411+
let registry = SectionRegistry::default_sections();
412+
assert_eq!(registry.builders.len(), 8);
413+
}
414+
415+
#[test]
416+
fn default_sections_env_present() {
417+
let registry = SectionRegistry::default_sections();
418+
let ctx = make_ctx();
419+
let result = registry.assemble(&ctx);
420+
assert!(result.contains("Environment"));
421+
assert!(result.contains("Working directory"));
422+
}
423+
424+
#[test]
425+
fn default_sections_with_git() {
426+
let registry = SectionRegistry::default_sections();
427+
let ctx = SectionContext {
428+
git_status: Some("On branch main, clean"),
429+
..make_ctx()
430+
};
431+
let result = registry.assemble(&ctx);
432+
assert!(result.contains("Git Status"));
433+
assert!(result.contains("On branch main"));
434+
// Git is dynamic, so boundary marker should be present
435+
assert!(result.contains(DYNAMIC_BOUNDARY_MARKER));
436+
}
437+
438+
#[test]
439+
fn default_sections_with_crab_md() {
440+
let registry = SectionRegistry::default_sections();
441+
let ctx = SectionContext {
442+
crab_md_content: Some("Build with cargo build"),
443+
..make_ctx()
444+
};
445+
let result = registry.assemble(&ctx);
446+
assert!(result.contains("CRAB.md"));
447+
assert!(result.contains("cargo build"));
448+
}
449+
450+
#[test]
451+
fn default_sections_with_memory() {
452+
let registry = SectionRegistry::default_sections();
453+
let ctx = SectionContext {
454+
memory_content: Some("User prefers Rust"),
455+
..make_ctx()
456+
};
457+
let result = registry.assemble(&ctx);
458+
assert!(result.contains("Memory"));
459+
assert!(result.contains("User prefers Rust"));
460+
}
461+
462+
#[test]
463+
fn default_sections_with_custom() {
464+
let registry = SectionRegistry::default_sections();
465+
let ctx = SectionContext {
466+
custom_instructions: Some("Always respond in Chinese"),
467+
..make_ctx()
468+
};
469+
let result = registry.assemble(&ctx);
470+
assert!(result.contains("Custom Instructions"));
471+
assert!(result.contains("Always respond in Chinese"));
472+
}
473+
474+
#[test]
475+
fn default_sections_skips_empty_tools() {
476+
let registry = SectionRegistry::default_sections();
477+
let ctx = make_ctx(); // tool_descriptions = ""
478+
let result = registry.assemble(&ctx);
479+
assert!(!result.contains("Available Tools"));
480+
}
481+
482+
#[test]
483+
fn default_sections_with_tools() {
484+
let registry = SectionRegistry::default_sections();
485+
let ctx = SectionContext {
486+
tool_descriptions: "- Read: reads files\n- Write: writes files",
487+
..make_ctx()
488+
};
489+
let result = registry.assemble(&ctx);
490+
assert!(result.contains("Available Tools"));
491+
assert!(result.contains("Read: reads files"));
492+
}
233493
}

0 commit comments

Comments
 (0)