Build a real agent in one JSON file. Tools, memory, sub-agents, and behavior — all declared, no code to write or compile.
Selena is a local-first agentic runtime. You describe an agent in agent.json, point it at a model you already run with Ollama or llama.cpp, and Selena drives the loop: the model calls tools, reads the results, and keeps going until the task is done.
No cloud. No API key. No data leaves your machine — the only network traffic is between Selena and your local model server.
> find all TODO comments in the codebase and summarise them
[tool: bash]
[tool done: bash ok]
Found 14 TODOs across 6 files. Most are in auth.rs and relate to
token expiry handling. Two are marked urgent.
You run a model locally and you want it to do things — run commands, read and write files, call your scripts — without standing up a Python project to glue it together. You tried a framework, and maintaining agent logic spread across a dozen source files got old.
With Selena, the entire agent lives in one JSON file. Change the model, swap the tools, add a sub-agent, rewrite the system prompt — edit JSON and run again. There is nothing to recompile.
If you're a Rust developer, the same engine ships as a library (selena-core) you can embed directly. That's a secondary path — see docs/EMBEDDING.md.
Selena is a runtime: a single binary that reads a config and runs an agent loop against a local model.
It is not:
- a chat UI or a coding IDE plugin
- a cloud product or hosted service
- a Python framework or a LangChain port
- a model server — it sits on top of Ollama or llama.cpp, it does not run the model itself
- production-hardened for multi-user or multi-tenant deployments
The honest alternatives for the "local model that uses tools" problem:
| Selena | Python frameworks (LangChain / LlamaIndex) | Raw Ollama API loop | |
|---|---|---|---|
| Where the agent lives | One JSON file | Spread across Python source files | Your own code |
| Code to write | None | Glue, classes, wiring | All of it |
| Tool calling loop | Built in | Built in | You write it |
| Add a tool | Drop a script + manifest in a folder | Write a Python class | Hand-wire it |
| Session memory & sub-agents | Built in | Varies by framework | You build it |
| Runs offline / local-only | Yes, by design | Usually, with setup | Yes |
| Language / runtime | Single Rust binary | Python environment | Whatever you wrote |
Python frameworks target a different ecosystem and are far broader; Selena is narrower on purpose. If you want declarative, local, no-code agents, that narrowness is the point.
- A local model server: Ollama or a llama.cpp server.
- A model that supports tool calling. This matters — Selena relies on the model emitting tool calls.
qwen2.5-coder,llama3.1, andmistral-nemoare reasonable choices; use whatever tool-capable model you have pulled and setmodelinagent.jsonto match. - The Rust toolchain (1.75+) — only if you build from source. Pre-built binaries for Linux, macOS, and Windows are on the Releases page.
The test suite passes on Linux, macOS, and Windows, and the built-ins are platform-aware:
bashruns viash -con Unix and PowerShell on Windows, andgrepfalls back to a pure-Rust search when the externalgrepbinary is absent. See Limitations.
Assumes Ollama is installed and running.
ollama pull qwen2.5-coder:7b # any tool-calling model worksGrab the archive for your platform from the Releases page — Linux (x86_64 / aarch64), macOS (Intel / Apple Silicon), and Windows. Extract it and put selena (or selena.exe) somewhere on your PATH. On macOS, if Gatekeeper blocks the unsigned binary, run xattr -d com.apple.quarantine selena.
git clone https://github.com/vlune/selena.git
cd selena
cargo build --releaseThe binary lands at target/release/selena. The first build compiles all dependencies and can take several minutes; later builds are incremental.
./target/release/selena # or just `selena` if you installed a release binaryNo config file is required for the first run — Selena falls back to a built-in default (currently configured for a llama.cpp server; copy and edit agent.json to point at your own server, e.g. Ollama on localhost:11434). You'll get a bare prompt:
>
> list the files in this directory and tell me which ones are Rust source files
[tool: bash]
[tool done: bash ok]
The directory contains Cargo.toml, agent.json, a crates/ directory, and a
target/ directory. The Rust source files live under crates/.
Type /quit or /exit to leave (or press Ctrl-D). Ctrl-C cancels the current turn without exiting.
Selena ran a loop between the model and your machine:
- You sent a message.
- The model decided it needed to run a command to answer.
- Selena ran the command on your machine and captured the output.
- The output went back to the model.
- The model answered based on what it saw.
That cycle — model requests a tool, Selena runs it, result returns to the model — repeats until the model is done or the iteration limit is hit. You never paste output back yourself.
Enable tools by listing them in the tools array. The defaults are:
| Tool | What it does |
|---|---|
bash |
Run a shell command, return stdout/stderr (sh -c on Unix, PowerShell on Windows) |
read_file |
Read a file's contents |
write_file |
Write content to a file, creating parent directories |
edit |
Replace an exact string in a file (unique match enforced) |
glob |
List files matching a glob pattern (in-process, cross-platform) |
grep |
Search files for a pattern (uses the grep binary when present, pure-Rust fallback otherwise) |
webfetch |
Fetch a URL and return it as text (HTML stripped); native, no external runtime |
todowrite / todoread |
Maintain a shared task list for the current session |
store_memory / retrieve_memory |
Save/read a named value in session memory |
remember / recall |
Persist/read facts across runs (project + global scopes) |
Auto-registered when configured: delegate_task / dispatch_parallel (sub-agents), lsp_diagnostics / lsp_hover (LSP), and any MCP server tools.
Full reference, inputs, and outputs: docs/TOOLS.md.
bashruns arbitrary shell commands. Only enable it where that is acceptable, and read docs/SECURITY.md before combining it withauto_accept.
Within a single run, the model can remember things you tell it (store_memory / retrieve_memory):
> remember that the database password is in .env.production
[tool: store_memory]
[tool done: store_memory ok]
Stored.
> where is the database password?
[tool: retrieve_memory]
[tool done: retrieve_memory ok]
You told me it is in .env.production.
That session memory clears when the process exits. For knowledge that should
survive restarts, enable persistent memory (memory.persistent.enabled) and
use the remember / recall tools — they write markdown to a project store
(.selena/memory) or a global store (~/.selena/memory), and the entries
are loaded back into the prompt at startup. See docs/CONFIG.md.
Add a tool by dropping a script and a manifest into a tools/ folder — no change to agent.json.
./target/release/selena tools scaffold weather # generates tools/weather/
# edit tools/weather/weather.sh (or weather.ps1 on Windows)
./target/release/selena tools doctor # validate the manifest + script
./target/release/selena tools trust weather # record its hash and trust itRun Selena again; the model now has a weather tool. Custom tools run as subprocesses that read JSON arguments on stdin and reply on stdout with {"success": true, "output": "..."}.
When you trust a tool, Selena records a SHA-256 hash of its manifest and script. If either changes, trust is automatically revoked on the next startup. Full manifest format and the trust model: docs/TOOLS.md.
Selena reads agent.json from the current directory (override with --config path/to/agent.json). If none is found, a built-in default is used. A fuller example:
{
"name": "my-agent",
"provider": "ollama", // "ollama", "llamacpp", or "custom" (any OpenAI-compatible API)
"model": "qwen2.5-coder:7b",
"system_prompt": "You are a helpful assistant.",
"tools": ["bash", "read_file", "write_file", "glob", "grep"],
"skills": ["store_memory", "retrieve_memory"],
"auto_accept": false, // require confirmation for gated tools (secure default)
"inference": {
"endpoint": "http://localhost:11434",
"temperature": 0.7,
"max_tokens": 4096,
"timeout_secs": 120
},
"context": { "max_tokens": 32768, "history_slots": 20, "reserve_output_tokens": 4096 },
"memory": { "max_segments": 50 },
"runtime": { "max_iterations": 20 },
"logging": { "level": "info", "format": "pretty" }
}
auto_acceptdefaults tofalse: tools that declarerequire_confirmationare blocked until you opt in withauto_accept: true. Withauto_accept: trueandbashenabled, the model can run any shell command unprompted. See docs/SECURITY.md.
Every field, type, and default: docs/CONFIG.md.
Real gaps. Read them before deciding whether Selena fits.
| Limitation | Detail |
|---|---|
| No kernel sandbox | sandbox.enabled gives subprocess tools a scrubbed, allowlisted environment (secrets aren't exposed), plus working-dir confinement. But capability labels (network, filesystem, execute) are still not OS-enforced — a tool labelled network: false can reach the network. Kernel isolation (seccomp/landlock, Job Objects) is on the roadmap. |
| Shell semantics differ on Windows | bash runs through PowerShell on Windows (and sh -c on Unix), so Unix-isms like ls -la or && fail there; a capable model recovers, but expect extra round-trips. grep prefers the external binary and falls back to a slower pure-Rust search on stock Windows. |
| Streaming is opt-in | The final answer is buffered by default; set runtime.stream: true for token-by-token output. Streaming recovers tool calls from content, which is lossless for Ollama/llama.cpp but falls back to buffered mode for native-only cloud providers. |
| No GUI | Command-line only. |
selena-core is a library crate with no dependency on the CLI. Embed it directly:
[dependencies]
selena-core = { path = "path/to/selena/crates/selena-core" }use selena_core::{Core, AgentConfig};
let config = AgentConfig::from_json(include_str!("agent.json"))?;
let mut core = Core::with_config(config)?; // construction is synchronous
let result = core.turn("summarise the files in /tmp").await?; // only turn() is async
println!("{}", result.content);Full API, events, custom providers, and registering tools programmatically: docs/EMBEDDING.md.
Recently shipped: native tool-call schemas for llama.cpp (the request now advertises tools/tool_choice), anchored auto-compaction (rolling conversation summary), Windows-native built-ins (bash via PowerShell, pure-Rust grep fallback), pre-built release binaries (TLS via rustls — no OpenSSL dependency), live MCP client, generic OpenAI-compatible provider, cross-session persistent memory, streaming turn output, interactive ask permissions, parallel + nested sub-agent dispatch, and LSP tools.
Planned: OS-level sandbox enforcement of capability labels, a durable (SQLite/embeddings) persistent-memory backend, and a richer TUI.
Full detail: docs/ROADMAP.md.
| docs/CONFIG.md | Every config field, type, default, and valid value |
| docs/TOOLS.md | Built-in tools, manifest format, trust model |
| docs/PROVIDERS.md | Ollama vs llama.cpp, tool-calling behavior, inspect-provider |
| docs/SECURITY.md | What is and isn't enforced, audit logging, risks |
| docs/EMBEDDING.md | Using selena-core as a Rust library |
| docs/EXAMPLES.md | Copy-paste agent configurations |
| docs/TROUBLESHOOTING.md | Common failures and fixes |
| docs/FAQ.md | Common questions |
| docs/ARCHITECTURE.md | Internal design for contributors |
| docs/ROADMAP.md | Built, in progress, and planned |
| docs/CONTRIBUTING.md | How to contribute |
Licensed under the Apache-2.0 License.