diff options
| author | Guillermo Serrahima <dev@vylion.com> | 2026-06-26 14:39:23 +0200 |
|---|---|---|
| committer | Guillermo Serrahima <dev@vylion.com> | 2026-06-26 14:39:23 +0200 |
| commit | 963f59569437fe43a49ce36a003a143fa2f767c8 (patch) | |
| tree | 968b0dad13193536770ae1e6b580b27e22f9366b /README.md | |
| parent | 741a04c2abcbd77451c0933a19d91fddb9982882 (diff) | |
| download | senia-963f59569437fe43a49ce36a003a143fa2f767c8.tar.xz senia-963f59569437fe43a49ce36a003a143fa2f767c8.zip | |
created custom agent loop
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 171 |
1 files changed, 171 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..f2e8b8b --- /dev/null +++ b/README.md @@ -0,0 +1,171 @@ +# Sénia + +a personal butler/coder AI assistant. + +runs in a docker container with persistent state, an HTTP API for chat, autonomous heartbeat behavior, and a memory system designed to mimic human-style recall (granular recent, fuzzy distant). + +## current state + +### running + +- **docker container, debian trixie**, `restart: unless-stopped` +- **senia user** (uid 750) with narrow `sudo` (apt/apt-get only) +- **claude code** as the current LLM backend (subscription-auth via mounted `~/.claude/`), installed system-wide so updates happen via container rebuild rather than self-update +- **read-only `:ro` mounts** enforce sandbox boundaries even against in-container root +- **HTTP API** on port 8000, OpenAI-compatible (`/v1/chat/completions`, `/v1/models`) + +### the agent loop (`senia/agent/`) + +- **fastapi service** running inside the container as the main process (uvicorn) +- **backend-agnostic design** via a `ModelBackend` strategy pattern: swapping models is a one-line change +- **current backend:** shells out to `claude -p` for non-interactive turns +- **dynamic per-turn context** assembled fresh: current time + today's diary entries +- **persona** handled by the backend natively (claude code autodetects `CLAUDE.md` from the workspace) +- **dependency management with `uv`**, a single static rust binary, dramatically faster than pip/poetry, uses standard PEP 621 metadata in pyproject.toml + +### memory + +- **diary log MCP tool:** short timestamped entries in sqlite, agent-authored throughout the day +- **task tracking MCP tool:** categorized by owner `agent` (to be done autonomously during heartbeats) or `user` (to periodically remind the user of) +- **single shared db** at `/etc/senia/data/senia.db`, migration-tracked +- **persona files** (SOUL.md / USER.md / AGENTS.md) for stable identity across model swaps + +### autonomous behavior + +- **variable heartbeat script** runs every 30 min via host cron + `docker exec` +- wakes Sénia when there are pending tasks OR more than 2 hours since the last wake +- captures her response and delivers it via **ntfy** push notification +- supports the `(quiet)` sentinel for no-op heartbeats +- runs orthogonal to the HTTP API; the heartbeat directly invokes claude code, doesn't pass through the loop + +### tooling for the agent herself + +- `uv` (preferred) and `pip --user` for python packages +- `npm install -g` for node tools (configured to her user-local npm prefix) +- `sudo apt install` for system packages, narrowly allowed + +### public / private repo split + +the persona content (`CLAUDE.md`) and saved conversation contexts contain real personal data that shouldn't live in a public portfolio. they're tracked in a separate private companion repository (`senia-private`), referenced as a git submodule at `private/`. + +the setup script handles both cases: + +- **with access to the private repo:** the submodule pulls down, real persona and contexts are mounted into the container +- **without access:** placeholder files are generated locally, the container runs in a demonstrable state with a generic persona + +both cases produce a fully working system; only the content differs. this preserves portfolio-readability while protecting personal data. + +## planned + +### chat interface + +- **Open WebUI** behind authentik for the primary chat surface (browser + mobile) +- **Conduit** as the native mobile client (FOSS, flutter, authentik-aware) +- the loop's HTTP API registered with Open WebUI as a custom OpenAI provider +- **ntfy** kept for asynchronous Sénia-initiated push (heartbeats, alerts) + +### memory expansion + +- **daily markdown summaries:** end-of-day compaction job consolidates raw logs into structured daily entries with frontmatter +- **monthly markdown summaries:** same pattern, dailies into a monthly digest +- **embeddings layer:** semantic retrieval via `sqlite-vec`, multilingual embedding model running locally (e.g. multilingual-e5-base via ollama) +- **session-start context injection:** already partially done via the loop; will expand as compaction lands + +### model strategy + +- **swap the backend to a local-first hybrid:** Qwen3.6 quantized via ollama as the default for english butler tasks +- **delegation pattern:** catalan inputs + coding tasks routed to a strong API model (Claude Sonnet) +- **OpenCode** as a tool Sénia can shell out to for LSP-aware code work +- expected to be a single new `OllamaBackend` class in `agent/orchestrator/models/` + routing logic in the loop + +### knowledge + +- **DokuWiki** as the private wiki (deferred for later) +- **Sénia reads via HTTP API**, not direct filesystem; DokuWiki handles link resolution and rendering +- **drafts to a staging area** for documentation help; user commits to wiki manually, preserving curation quality + +## project structure + +``` +~/senia/ +├── .gitmodules # references the private submodule +├── .gitignore +├── .dockerignore # at project root (the docker build context) +├── README.md +├── scripts/ +│ └── setup.sh # bridges public + private repos with fallback +├── docker/ +│ ├── compose.yml +│ └── Dockerfile +├── claude/ +│ └── CLAUDE.example.md # placeholder persona, tracked publicly +├── workspace/ # mounted as /workspace, Sénia's per-turn cwd +├── data/ # runtime state, owned by senia user +│ ├── senia.db # single sqlite db (diary, tasks, future tables) +│ ├── memory/ # future: daily and monthly summaries +│ ├── heartbeat/ # heartbeat state + logs +│ └── scripts/ +│ └── heartbeat.py # variable cron-driven wake-up +├── mcp/ # MCP server source, mounted :ro +│ └── senia/ +│ ├── server.py +│ ├── db.py +│ ├── memory/ +│ │ └── diary.py +│ ├── tasks.py +│ └── migrations/ +├── private/ # submodule: real CLAUDE.md + contexts/ +│ # (gitignored except for .gitmodules entry) +└── agent/ # the orchestration loop, baked into the image + ├── pyproject.toml + ├── uv.lock + ├── README.md + └── orchestrator/ + ├── api.py # fastapi entrypoint + ├── loop.py # per-turn orchestration + ├── context.py # dynamic per-turn context builder + └── models/ + ├── base.py # ModelBackend ABC, ModelRequest/Response types + └── claude_code.py # current backend +``` + +## working with the project + +### first-time setup + +```bash +git clone git@github.com:vylion/senia.git +cd senia +./scripts/setup.sh # initializes private submodule or generates placeholders +cd docker +docker compose build +docker compose up -d +docker compose logs -f senia +``` + +### updating agent code + +the agent code (everything under `agent/`) is **baked into the image at build time**, not mounted at runtime. so changes to `agent/` require rebuilding: + +```bash +cd ~/senia/docker +docker compose build senia +docker compose up -d +``` + +thanks to docker's layer caching (dependencies are copied/installed in a separate step from application code), rebuilds where only python code changed complete in seconds. only changes to `pyproject.toml` or `uv.lock` trigger a full dependency reinstall. + +other things that don't require rebuild: + +- the persona files, mcp server code, data, and workspace: all mounted at runtime +- the heartbeat script (lives on the host, runs via `docker exec`) + +## design principles + +- **own the loop where dynamic context injection matters; use mature tools where they earn their keep** (e.g. OpenCode for LSP-aware coding when added, Open WebUI for chat UI polish) +- **memory hierarchy mirrors human recall:** granular recent, compressed distant, semantic retrieval on demand +- **model is swappable; system stays the same:** persona, tools, and memory all model-agnostic +- **container is the security boundary:** in-container non-root user + `:ro` mounts give Sénia freedom in her sandbox without compromising host +- **diary as audit trail:** installations, decisions, and observations all logged, so the user can review what's been changed +- **orthogonal channels:** chat goes through the loop's HTTP API; heartbeats invoke claude code directly. each channel uses the simplest path that works +- **separation of public code and private data:** architecture is open, content is private, both versioned |
