Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions crates/ov_cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -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`

67 changes: 67 additions & 0 deletions crates/ov_cli/src/commands/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<serde_json::Value>(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")
Expand Down
15 changes: 15 additions & 0 deletions crates/ov_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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 => {
Expand Down
Loading