aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: f2e8b8bd93a3d3f8bd5457fe42bbef93a1862e07 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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