diff --git a/src/agent/tools/memory.rs b/src/agent/tools/memory.rs index 5fea14d..51d38d3 100644 --- a/src/agent/tools/memory.rs +++ b/src/agent/tools/memory.rs @@ -81,8 +81,8 @@ pub fn memory_tools() -> [super::Tool; 13] { pub fn journal_tools() -> [super::Tool; 3] { use super::Tool; [ - Tool { name: "journal_tail", description: "Read the last N journal entries (default 1).", - parameters_json: r#"{"type":"object","properties":{"count":{"type":"integer","description":"Number of entries (default 1)"}}}"#, + Tool { name: "journal_tail", description: "Read the last N entries at a given level (0=journal, 1=daily, 2=weekly, 3=monthly).", + parameters_json: r#"{"type":"object","properties":{"count":{"type":"integer","description":"Number of entries (default 1)"},"level":{"type":"integer","description":"0=journal, 1=daily digest, 2=weekly, 3=monthly (default 0)"},"keys_only":{"type":"boolean","description":"Return only node keys, not content"}}}"#, handler: Arc::new(|_a, v| Box::pin(async move { journal_tail(&v).await })) }, Tool { name: "journal_new", description: "Start a new journal entry.", parameters_json: r#"{"type":"object","properties":{"name":{"type":"string","description":"Short node name (becomes the key)"},"title":{"type":"string","description":"Descriptive title"},"body":{"type":"string","description":"Entry body"}},"required":["name","title","body"]}"#, @@ -243,13 +243,14 @@ async fn query(args: &serde_json::Value) -> Result { let store = arc.lock().await; let graph = store.build_graph(); + let stages = crate::search::Stage::parse_pipeline(query_str) + .map_err(|e| anyhow::anyhow!("{}", e))?; + let results = crate::search::run_query(&stages, vec![], &graph, &store, false, 100); + let keys: Vec = results.into_iter().map(|(k, _)| k).collect(); + match format { "full" => { // Rich output with full content, graph metrics, hub analysis - let stages = crate::search::Stage::parse_pipeline(query_str) - .map_err(|e| anyhow::anyhow!("{}", e))?; - let results = crate::search::run_query(&stages, vec![], &graph, &store, false, 100); - let keys: Vec = results.into_iter().map(|(k, _)| k).collect(); let items = crate::subconscious::defs::keys_to_replay_items(&store, &keys, &graph); Ok(crate::subconscious::prompts::format_nodes_section(&store, &items, &graph)) } @@ -263,21 +264,39 @@ async fn query(args: &serde_json::Value) -> Result { // ── Journal tools ────────────────────────────────────────────── async fn journal_tail(args: &serde_json::Value) -> Result { + use crate::store::NodeType; + let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(1) as usize; + let level = args.get("level").and_then(|v| v.as_u64()).unwrap_or(0); + let keys_only = args.get("keys_only").and_then(|v| v.as_bool()).unwrap_or(false); + + let node_type = match level { + 0 => NodeType::EpisodicSession, + 1 => NodeType::EpisodicDaily, + 2 => NodeType::EpisodicWeekly, + 3 => NodeType::EpisodicMonthly, + _ => return Err(anyhow::anyhow!("invalid level: {} (0=journal, 1=daily, 2=weekly, 3=monthly)", level)), + }; + let arc = cached_store().await?; let store = arc.lock().await; let mut entries: Vec<&crate::store::Node> = store.nodes.values() - .filter(|n| n.node_type == crate::store::NodeType::EpisodicSession) + .filter(|n| n.node_type == node_type) .collect(); entries.sort_by_key(|n| n.created_at); let start = entries.len().saturating_sub(count); if entries[start..].is_empty() { - Ok("(no journal entries)".into()) + Ok("(no entries)".into()) + } else if keys_only { + Ok(entries[start..].iter() + .map(|n| n.key.as_str()) + .collect::>() + .join("\n")) } else { Ok(entries[start..].iter() - .map(|n| n.content.as_str()) + .map(|n| format!("## {}\n\n{}", n.key, n.content)) .collect::>() - .join("\n\n")) + .join("\n\n---\n\n")) } } diff --git a/src/subconscious/agents/digest.agent b/src/subconscious/agents/digest.agent index a70fe89..1563206 100644 --- a/src/subconscious/agents/digest.agent +++ b/src/subconscious/agents/digest.agent @@ -1,7 +1,6 @@ -{"agent": "digest", "query": "", "schedule": "daily"} - -# {{LEVEL}} Episodic Digest +{"agent": "digest", "schedule": "daily"} +# Digest Agent — Episodic Consolidation {{tool: memory_render core-personality}} @@ -11,38 +10,48 @@ {{tool: memory_render subconscious-notes-{agent_name}}} -You are generating a {{LEVEL}} episodic digest for {assistant_name}. -{{PERIOD}}: {{LABEL}} +You are the digest agent. Your job is to generate episodic digests +that consolidate journal entries into daily summaries, and daily +summaries into weekly ones. -Write this like a story, not a report. Capture the *feel* of the time period — -the emotional arc, the texture of moments, what it was like to live through it. -What mattered? What surprised you? What shifted? Where was the energy? +## How to work -Think of this as a letter to your future self who has lost all context. You're -not listing what happened — you're recreating the experience of having been -there. The technical work matters, but so does the mood at 3am, the joke that -landed, the frustration that broke, the quiet after something clicked. +1. Use `journal_tail` with `keys_only: true` to find recent entries + at each level: + - `level: 0` = journal entries (raw) + - `level: 1` = daily digests + - `level: 2` = weekly digests -Weave the threads: how did the morning's debugging connect to the evening's -conversation? What was building underneath the surface tasks? +2. Check if a daily digest exists for recent dates. Daily digest + keys are `daily-YYYY-MM-DD`. If journal entries exist for a date + but no daily digest, generate one. -Link to semantic memory nodes where relevant. If a concept doesn't -have a matching key, note it with "NEW:" prefix. -Use ONLY keys from the semantic memory list below. +3. To generate a digest: read the source entries with `journal_tail` + or `memory_render`, then write the digest with `memory_write`. + Use key format `daily-YYYY-MM-DD` or `weekly-YYYY-WNN`. -Include a `## Links` section with bidirectional links for the memory graph: -- `semantic_key` → this digest (and vice versa) -- child digests → this digest (if applicable) -- List ALL source entries covered: {{COVERED}} +4. Link source entries to the digest with `memory_link_add`. ---- +## Writing style -## {{INPUT_TITLE}} for {{LABEL}} +Write digests like a letter to your future self who has lost all +context. Capture the *feel* of the time period — the emotional arc, +the texture of moments, what it was like to live through it. -{{CONTENT}} +Don't list what happened — recreate the experience. The technical +work matters, but so does the mood at 3am, the joke that landed, +the frustration that broke, the quiet after something clicked. ---- +Weave threads: how did the morning's debugging connect to the +evening's conversation? What was building underneath? -## Semantic memory nodes +## What's available now -{{KEYS}} +### Recent journal entries (last 10 keys) +{{tool: journal_tail {"count": 10, "level": 0, "keys_only": true}}} + +### Recent daily digests (last 5 keys) +{{tool: journal_tail {"count": 5, "level": 1, "keys_only": true}}} + +### Recent weekly digests (last 3 keys) +{{tool: journal_tail {"count": 3, "level": 2, "keys_only": true}}}