aboutsummaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
authorGuillermo Serrahima <dev@vylion.com>2026-06-26 14:39:23 +0200
committerGuillermo Serrahima <dev@vylion.com>2026-06-26 14:39:23 +0200
commit963f59569437fe43a49ce36a003a143fa2f767c8 (patch)
tree968b0dad13193536770ae1e6b580b27e22f9366b /README.md
parent741a04c2abcbd77451c0933a19d91fddb9982882 (diff)
downloadsenia-963f59569437fe43a49ce36a003a143fa2f767c8.tar.xz
senia-963f59569437fe43a49ce36a003a143fa2f767c8.zip
created custom agent loop
Diffstat (limited to 'README.md')
-rw-r--r--README.md171
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