diff --git a/src/agent/tools/memory.rs b/src/agent/tools/memory.rs index 9a36799..f805409 100644 --- a/src/agent/tools/memory.rs +++ b/src/agent/tools/memory.rs @@ -43,6 +43,29 @@ pub fn definitions() -> Vec { sorting, field selection. Examples: \"degree > 10 | sort weight | limit 5\", \ \"neighbors('identity') | select strength\", \"key ~ 'journal.*' | count\"", json!({"type":"object","properties":{"query":{"type":"string","description":"Query expression"}},"required":["query"]})), + ToolDef::new("output", + "Produce a named output value. Use this to pass structured results \ + between steps — subsequent prompts can see these in the conversation history.", + json!({"type":"object","properties":{ + "key":{"type":"string","description":"Output name (e.g. 'relevant_memories')"}, + "value":{"type":"string","description":"Output value"} + },"required":["key","value"]})), + ToolDef::new("journal_tail", + "Read the last N journal entries (default 1).", + json!({"type":"object","properties":{ + "count":{"type":"integer","description":"Number of entries (default 1)"} + }})), + ToolDef::new("journal_new", + "Start a new journal entry with a ## heading and body.", + json!({"type":"object","properties":{ + "title":{"type":"string","description":"Entry title (becomes ## YYYY-MM-DDTHH:MM — title)"}, + "body":{"type":"string","description":"Entry body (2-3 paragraphs)"} + },"required":["title","body"]})), + ToolDef::new("journal_update", + "Append text to the most recent journal entry (same thread continuing).", + json!({"type":"object","properties":{ + "body":{"type":"string","description":"Text to append to the last entry"} + },"required":["body"]})), ] } @@ -114,6 +137,75 @@ pub fn dispatch(name: &str, args: &serde_json::Value, provenance: Option<&str>) crate::query_parser::query_to_string(&store, &graph, query) .map_err(|e| anyhow::anyhow!("{}", e)) } + "output" => { + let key = get_str(args, "key")?; + let value = get_str(args, "value")?; + let dir = std::env::var("POC_AGENT_OUTPUT_DIR") + .map_err(|_| anyhow::anyhow!("no output directory set"))?; + let path = std::path::Path::new(&dir).join(key); + std::fs::write(&path, value) + .with_context(|| format!("writing output {}", path.display()))?; + Ok(format!("{}: {}", key, value)) + } + "journal_tail" => { + let count = args.get("count").and_then(|v| v.as_u64()).unwrap_or(1) as usize; + let store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?; + let content = store.nodes.get("journal") + .map(|n| n.content.as_str()) + .unwrap_or(""); + let mut entries: Vec<&str> = Vec::new(); + let mut remaining = content; + while let Some(pos) = remaining.rfind("\n## ") { + entries.push(&remaining[pos + 1..]); + remaining = &remaining[..pos]; + if entries.len() >= count { break; } + } + if entries.len() < count && remaining.starts_with("## ") { + entries.push(remaining); + } + entries.reverse(); + if entries.is_empty() { + Ok("(no journal entries)".into()) + } else { + Ok(entries.join("\n\n")) + } + } + "journal_new" => { + let title = get_str(args, "title")?; + let body = get_str(args, "body")?; + let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M"); + let entry = format!("## {} — {}\n\n{}", ts, title, body); + let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?; + let existing = store.nodes.get("journal") + .map(|n| n.content.clone()) + .unwrap_or_default(); + let new_content = if existing.is_empty() { + entry.clone() + } else { + format!("{}\n\n{}", existing.trim_end(), entry) + }; + store.upsert_provenance("journal", &new_content, prov) + .map_err(|e| anyhow::anyhow!("{}", e))?; + store.save().map_err(|e| anyhow::anyhow!("{}", e))?; + let word_count = body.split_whitespace().count(); + Ok(format!("New entry '{}' ({} words)", title, word_count)) + } + "journal_update" => { + let body = get_str(args, "body")?; + let mut store = Store::load().map_err(|e| anyhow::anyhow!("{}", e))?; + let existing = store.nodes.get("journal") + .map(|n| n.content.clone()) + .unwrap_or_default(); + if existing.is_empty() { + anyhow::bail!("no journal entry to update — use journal_new first"); + } + let new_content = format!("{}\n\n{}", existing.trim_end(), body); + store.upsert_provenance("journal", &new_content, prov) + .map_err(|e| anyhow::anyhow!("{}", e))?; + store.save().map_err(|e| anyhow::anyhow!("{}", e))?; + let word_count = body.split_whitespace().count(); + Ok(format!("Updated last entry (+{} words)", word_count)) + } _ => anyhow::bail!("Unknown memory tool: {}", name), } }