Architecture
The headless core, the domain layer, the IPC protocol and the agent loop.
Architecture
Sayri is a Python package with a hexagonal core. The headless brain knows nothing about windows; every interface attaches to it through one event sink.
The core
sayri/core.py defines SayriCore, the headless engine. It owns the voice loop (STT session), TTS, the ReAct agent engine, sandbox, storage, wake word handling, sessions and remote/gateway message processing. It emits state changes through a CoreUI sink — an object with no-op callbacks:
on_ready, on_state, on_audio_level, on_mic, on_busy, on_partial,
on_user, on_assistant_delta, on_assistant_done, on_tool_start,
on_tool_finish, on_hint, on_error, on_speaking, on_show, on_hide,
on_conversation_started, on_shutdown
Any UI — the GTK orb, a Clippy widget, the CLI, a web bubble — subclasses or wraps this sink. Nothing else is required to build a new front-end.
The domain layer
sayri/domain/ holds the pieces that make decisions:
| Module | Responsibility |
|---|---|
models.py |
Dataclasses: AgentProfile, SandboxConfig, SandboxLevel, Session, PluginManifest, SecurityAuditReport |
agent_engine.py |
ReAct loop: builds the system prompt, runs tool calls, feeds observations back, persists messages |
agent_creator.py |
Creates sub-agents from natural language, with privilege containment |
permissions.py |
Rule engine: {action, resource, effect} with whole-value wildcards, last match wins |
permission_broker.py |
The ask/checkpoint flow: a card with Allow / Allow always / Deny |
skills_scanner.py |
Static pre-install audit of skills and plugins, with a risk score |
secrets_manager.py |
Zero-plaintext vault: secrets never reach the LLM prompt or chat history |
cron_scheduler.py |
Routines: on_login, daily_at, hourly triggers that run prompts |
triggers.py |
System event triggers stored as JSON files |
The adapters
sayri/adapters/ holds the boundaries:
sandbox/executor.py—SandboxExecutor, the only code that runs commands. Enforces the level, the permission rules and the policies before anything executes.storage/sqlite_sessions.py— sessions, messages, learned preferences and security authorizations in one SQLite file.
Process model
There are three ways Sayri runs, and they can coexist:
- GUI (
python3 -m sayriorsayri): GTK4 overlay with the orb and the cajita. Owns the legacy socketsayri.sock. - Daemon (
sayri.daemon): headless. OwnsSayriCoreand exposes it onsayri-daemon.sock. It also keeps a legacy listener onsayri.sockspeaking the old gateway wire protocol so channel gateways keep working without changes. - CLI (
sayri.cli): one-shot commands over the same core.
Every core event is broadcast to all connected IPC clients, so a Clippy widget and the GTK orb see exactly the same stream.
IPC protocol
JSON over a local socket, one message per line (NDJSON), defined in sayri/ipc.py.
Requests: {"cmd": "talk", "params": {"text": "..."}, "id": 1}
Responses: {"ok": true, "result": {...}, "id": 1}
Events broadcast to every client: ready, state, audio_level, mic, busy, partial, user, assistant_delta, assistant_done, tool_start, tool_finish, hint, error, speaking, shutdown.
Commands: ping, status, talk, ask, listen, stop_listening, toggle_listening, interrupt, new_conversation, switch_session, config_get, config_set, config_list, skills_list, skills_install, skills_uninstall, skills_search, plugins_list, gateway_list, gateway_start, gateway_stop, gateway_delete, gateway_save, agents_list, version, quit — plus the permission, settings, routines, vault and UI commands the cajita panel uses.
The socket is chmod 600 and rejects connections from other users (peer UID validation).
The agent loop
AgentEngine.process_query runs in a background thread:
- Detects sub-agent or skill-install intent in natural language and refuses it from restricted levels (levels 0–2 cannot create sub-agents or install anything).
- Builds the system prompt: profile, sandbox level in plain language, installed skills, tool restrictions, and the investigation-loop protocol if enabled.
- Compacts history: the last 4 messages verbatim, older ones as a summary,
search_historyas an escape hatch. - Streams from the OpenAI-compatible endpoint. When the reply contains a
```bashblock, extracts it, checks permissions, executes through the sandbox, sanitizes the output for secrets, and feeds it back as an observation — up to 6 steps (10 with the investigation loop). - Anything longer than 4 seconds is treated as launched in the background (for GUI apps and
xdg-open). - Root operations go through
pkexec, which shows a graphical Polkit dialog the user must approve. - With
reinforcement_learningon, substantive commands are recorded as learned preferences queryable withsayri-pref.
Multi-agent
Agents are JSON profiles in ~/.config/sayri/agents/. Each profile carries its own model config, sandbox config (level, rules, blocklists, ask switch), allowed skills, allowed plugins, allowed tools, custom instructions, and the investigation/learning switches. The engine enforces non-escalation: an agent at level 0–2 cannot create sub-agents, and a sub-agent can never get a level higher than its creator.
Gateway instances bind a channel to an agent: one process per instance, managed by the GatewaySupervisor. Several gateways, agents and sandbox levels run in parallel.