use cel_memory::{MemoryChunk, SummaryContext};
pub(crate) const DEFAULT_MAX_WORDS: u32 = 200;
pub(crate) fn build_system_prompt(ctx: &SummaryContext) -> String {
let max_words = ctx.max_words.unwrap_or(DEFAULT_MAX_WORDS);
let unit = ctx.kind_label.as_deref().unwrap_or("session");
format!(
"You are a memory summarizer for an AI agent.\n\
Produce concise, high-recall summaries that the agent will later search for\n\
the user's situation, decisions, and outcomes — not verbose paraphrases.\n\
\n\
Produce a {unit} summary covering:\n\
- What the user was trying to do\n\
- What the agent or rules actually did\n\
- Any user corrections or surprises\n\
- Outcome (success/failure/ongoing)\n\
- Concrete nouns: file paths, app names, URLs, named entities\n\
\n\
Maximum {max_words} words. Use neutral past tense. Begin with the most important sentence."
)
}
pub(crate) fn build_user_prompt(chunks: &[MemoryChunk], ctx: &SummaryContext) -> String {
let mut s = String::with_capacity(chunks.len() * 200);
s.push_str("Memory chunks (oldest first):\n\n");
for (i, c) in chunks.iter().enumerate() {
let ts = c.created_at.to_rfc3339();
s.push_str(&format!(
"[{idx}] {ts} kind={kind:?} caller={caller}\n{content}\n\n",
idx = i + 1,
ts = ts,
kind = c.kind,
caller = c.caller_id,
content = c.content.trim()
));
}
if let Some(note) = &ctx.note {
s.push_str("Note: ");
s.push_str(note);
s.push('\n');
}
s
}
pub(crate) fn max_tokens_for_context(ctx: &SummaryContext) -> u32 {
let max_words = ctx.max_words.unwrap_or(DEFAULT_MAX_WORDS);
((max_words as f32 * 1.4).ceil() as u32 + 64).max(256)
}
#[cfg(test)]
mod tests {
use super::*;
use cel_memory::{ChunkKind, ChunkSource, MemoryTier};
use chrono::Utc;
use serde_json::Value;
fn chunk(content: &str) -> MemoryChunk {
MemoryChunk {
id: "c1".into(),
created_at: Utc::now(),
kind: ChunkKind::Chat,
tier: MemoryTier::Session,
source: ChunkSource::Embedded,
session_id: Some("s1".into()),
project_root: None,
caller_id: "embedded".into(),
content: content.into(),
metadata: Value::Null,
importance: 0.5,
pinned: false,
shareable: false,
superseded_by: None,
embedding_model: "mock".into(),
embedding_dim: 0,
}
}
#[test]
fn system_prompt_includes_kind_label_and_word_cap() {
let sys = build_system_prompt(&SummaryContext {
kind_label: Some("day 2026-05-23".into()),
max_words: Some(120),
..Default::default()
});
assert!(sys.contains("day 2026-05-23 summary"));
assert!(sys.contains("Maximum 120 words"));
}
#[test]
fn user_prompt_includes_chunks_and_note() {
let user = build_user_prompt(
&[chunk("alpha")],
&SummaryContext {
note: Some("first daily rollup".into()),
..Default::default()
},
);
assert!(user.contains("alpha"));
assert!(user.contains("first daily rollup"));
}
}