(WIP) custom agent
  • Python 91.7%
  • Dockerfile 6.9%
  • Shell 1.4%
Find a file
2026-07-02 09:23:54 +02:00
agent Added matrix comm channel 2026-07-02 09:23:54 +02:00
claude first commit 2026-06-26 13:40:00 +02:00
docker Added matrix comm channel 2026-07-02 09:23:54 +02:00
mcp/senia created custom agent loop 2026-06-26 14:39:23 +02:00
private@d6e1b114f5 More private data location tweaks 2026-07-02 09:23:28 +02:00
scripts tweaked compaction 2026-07-02 00:04:12 +02:00
.dockerignore created custom agent loop 2026-06-26 14:39:23 +02:00
.gitignore More private data location tweaks 2026-07-02 09:23:28 +02:00
.gitmodules added private submodule for persona + contexts 2026-06-26 13:49:09 +02:00
README.md Updated README 2026-07-01 12:45:47 +02:00

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.5 hours since the last wake (5 hours since last wake during the night)
  • captures her response and delivers it via ntfy push notification
  • 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

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:

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