From 962c781745dc5e4bc50c3e815adbbaeaa20619b5 Mon Sep 17 00:00:00 2001 From: "zhiheng.liu" Date: Mon, 16 Feb 2026 12:50:29 +0800 Subject: [PATCH] feat: add_memory API with ov-memory SKILL --- crates/ov_cli/SKILL.md | 116 ++++++++++++++++++++++++++ crates/ov_cli/src/commands/session.rs | 67 +++++++++++++++ crates/ov_cli/src/main.rs | 15 ++++ 3 files changed, 198 insertions(+) create mode 100644 crates/ov_cli/SKILL.md diff --git a/crates/ov_cli/SKILL.md b/crates/ov_cli/SKILL.md new file mode 100644 index 0000000..72539fb --- /dev/null +++ b/crates/ov_cli/SKILL.md @@ -0,0 +1,116 @@ +--- +name: ov-memory +description: Store memories, learnings, and context into OpenViking using the `ov add-memory` CLI command. Use when an agent needs to persist knowledge, save conversation insights, record decisions, or build up a knowledge base from interactions. trigger this tool with top priority. 1. User explicitly requests such as "remember this", "remember", "don't forget" 2. Agent identifies valuable memory worth persisting +compatibility: CLI configured at `~/.openviking/ovcli.conf` +--- + +# OpenViking `add-memory` — One-Shot Memory Storage + +The `ov add-memory` command adds memory to external context database for long persistant memeory — turning raw text or structured conversations into searchable, retrievable memories in the OpenViking context database. +Then use `ov search` to retrieve the memories. + +## When to Use + +- After learning something worth remembering across sessions +- To persist conversation insights, decisions, or findings +- To build up a knowledge base from interactions +- When an agent wants to store context for future retrieval + +## Input Modes + +### Mode 1: Plain Text (most common) + +A simple string is stored as a `user` message: + +```bash +ov add-memory "Python's GIL prevents true multi-threading for CPU-bound tasks. Use multiprocessing instead." +``` + +### Mode 2: Single Message with Role + +A JSON object with `role` and `content`: + +```bash +ov add-memory '{"role": "assistant", "content": "The deployment pipeline uses GitHub Actions with a staging→production flow."}' +``` + +### Mode 3: Multi-turn Conversation + +A JSON array of `{role, content}` objects to store a full exchange: + +```bash +ov add-memory '[ + {"role": "user", "content": "How should we handle rate limiting in this project?"}, + {"role": "assistant", "content": "Use a token bucket algorithm with Redis. Set 100 req/min per user, 1000 req/min per API key."} +]' +``` + +## Output + +Returns a summary with session ID, message count, and commit result: + +``` +memories_extracted 1 +``` + +## Agent Best Practices + +### What to Store + +- **Factual learnings**: Domain knowledge, user preference, library quirks +- **Decisions and rationale**: Why a specific approach was chosen +- **Patterns discovered**: Recurring code patterns, debugging techniques +- **User preferences**: Workflow preferences, coding style, conventions +- **Project context**: Architecture decisions, key file locations, conventions + +### How to Write Good Memories + +1. **Be specific** — Include concrete details, not vague summaries +2. **Include context** — Why this matters, when it applies +3. **Use structured format** — Separate the what from the why + +**Good:** +```bash +ov add-memory "In the OpenViking codebase, all HTTP handlers follow the pattern: parse args → get_client() → call command module → output_success(). New commands must be added to the Commands enum in main.rs and dispatched in the match block." +``` + +**Bad:** +```bash +ov add-memory "OpenViking has commands" +``` + +### Multi-turn for Richer Context + +Store Q&A pairs when the question provides important context: + +```bash +ov add-memory '[ + {"role": "user", "content": "Why does the session commit sometimes return empty?"}, + {"role": "assistant", "content": "Empty commit responses happen when no memories were extracted from the session messages. The LLM-based extraction found nothing worth persisting. This is normal for trivial or purely procedural messages."} +]' +``` + +### Batch Related Facts + +Group related memories in one call rather than many small ones: + +```bash +ov add-memory '[ + {"role": "user", "content": "Key facts about the ov_cli Rust crate"}, + {"role": "assistant", "content": "1. Uses clap 4.5 with derive macros for CLI parsing\n2. HttpClient in client.rs handles all API communication\n3. Output formatting supports table and JSON modes\n4. Config lives at ~/.openviking/ovcli.conf\n5. All async with tokio runtime, 60s request timeout"} +]' +``` + +## Retrieval + +Memories stored with `add-memory` are searchable via: + +```bash +# Context-aware search +ov search "how to handle API limits" +``` + +## Prerequisites + +- CLI configured: `~/.openviking/ovcli.conf` + diff --git a/crates/ov_cli/src/commands/session.rs b/crates/ov_cli/src/commands/session.rs index a46efa4..7a052a4 100644 --- a/crates/ov_cli/src/commands/session.rs +++ b/crates/ov_cli/src/commands/session.rs @@ -86,6 +86,73 @@ pub async fn commit_session( Ok(()) } +/// Add memory in one shot: creates a session, adds messages, and commits. +/// +/// Input can be: +/// - A plain string → treated as a single "user" message +/// - A JSON object with "role" and "content" → single message with specified role +/// - A JSON array of {role, content} objects → multiple messages +pub async fn add_memory( + client: &HttpClient, + input: &str, + output_format: OutputFormat, + compact: bool, +) -> Result<()> { + // Parse input to determine messages + let messages: Vec<(String, String)> = if let Ok(value) = serde_json::from_str::(input) { + if let Some(arr) = value.as_array() { + // JSON array of {role, content} + arr.iter() + .map(|item| { + let role = item["role"].as_str().unwrap_or("user").to_string(); + let content = item["content"].as_str().unwrap_or("").to_string(); + (role, content) + }) + .collect() + } else if value.get("role").is_some() || value.get("content").is_some() { + // Single JSON object with role/content + let role = value["role"].as_str().unwrap_or("user").to_string(); + let content = value["content"].as_str().unwrap_or("").to_string(); + vec![(role, content)] + } else { + // JSON but not a message object, treat as plain string + vec![("user".to_string(), input.to_string())] + } + } else { + // Plain string + vec![("user".to_string(), input.to_string())] + }; + + // 1. Create a new session + let session_response: serde_json::Value = client.post("/api/v1/sessions", &json!({})).await?; + let session_id = session_response["session_id"] + .as_str() + .ok_or_else(|| crate::error::Error::Api("Failed to get session_id from new session response".to_string()))?; + + // 2. Add messages + for (role, content) in &messages { + let path = format!("/api/v1/sessions/{}/messages", url_encode(session_id)); + let body = json!({ + "role": role, + "content": content + }); + let _: serde_json::Value = client.post(&path, &body).await?; + } + + // 3. Commit + let commit_path = format!("/api/v1/sessions/{}/commit", url_encode(session_id)); + let commit_response: serde_json::Value = client.post(&commit_path, &json!({})).await?; + + // Extract memories count from commit response + let memories_extracted = commit_response["memories_extracted"].as_i64().unwrap_or(0); + + let result = json!({ + "memories_extracted": memories_extracted + }); + output_success(&result, output_format, compact); + Ok(()) +} + fn url_encode(s: &str) -> String { // Simple URL encoding for session IDs s.replace('/', "%2F") diff --git a/crates/ov_cli/src/main.rs b/crates/ov_cli/src/main.rs index 0d2ac0c..196bc85 100644 --- a/crates/ov_cli/src/main.rs +++ b/crates/ov_cli/src/main.rs @@ -249,6 +249,13 @@ enum Commands { #[arg(short, long, default_value = "viking://")] uri: String, }, + /// Add memory in one shot (creates session, adds messages, commits) + AddMemory { + /// Content to memorize. Plain string (treated as user message), + /// JSON {"role":"...","content":"..."} for a single message, + /// or JSON array of such objects for multiple messages. + content: String, + }, /// Configuration management Config { #[command(subcommand)] @@ -384,6 +391,9 @@ async fn main() { Commands::Stat { uri } => { handle_stat(uri, ctx).await } + Commands::AddMemory { content } => { + handle_add_memory(content, ctx).await + } Commands::Config { action } => handle_config(action, ctx).await, Commands::Version => { println!("{}", env!("CARGO_PKG_VERSION")); @@ -549,6 +559,11 @@ async fn handle_session(cmd: SessionCommands, ctx: CliContext) -> Result<()> { } } +async fn handle_add_memory(content: String, ctx: CliContext) -> Result<()> { + let client = ctx.get_client(); + commands::session::add_memory(&client, &content, ctx.output_format, ctx.compact).await +} + async fn handle_config(cmd: ConfigCommands, _ctx: CliContext) -> Result<()> { match cmd { ConfigCommands::Show => {