Development notes

For people working on eVi itself.

Setting up

python -m venv .venv
.venv\Scripts\Activate.ps1            # Windows PowerShell
# source .venv/bin/activate            # Linux / macOS
pip install -e '.[dev,web,mcp,scheduler,downloads,web-tools,stt,computer]'

The stt and computer extras pull native deps (PortAudio for sounddevice, screen capture libs for pyautogui). If you're not actively touching those areas, skip them.

Test suite

pytest -q                         # ~12 s, 183 tests
pytest -q tests/test_agent.py     # one file
pytest -q -k "permission"         # by keyword
pytest --timeout=15               # ceiling per test

The suite avoids network calls and external processes by:

Linting

ruff check evi apps tests
ruff format evi apps tests        # if you want formatter behavior too

pyproject.toml sets line-length = 100, target-version = "py311".

Project layout

evi/                core library — no IO at import time
  agent.py           Agent class + Event types
  backends/          LM Studio / Ollama / llama.cpp / OpenAI-compat
  commands.py        ~/.evi/commands/ loader
  config.py          Config dataclasses + load/save + dir constants
  dream.py           memory consolidation engine
  downloads.py       HF GGUF puller
  hardware.py        NVIDIA + RAM detection
  hooks.py           hook config loader + runner
  llm/
    agent.py         re-exports from evi.agent (or holds the loop)
    client.py        make_client(settings) → OpenAI SDK
    subagent.py      run_subagent() + SUBAGENT_PROFILES dict
  mcp/
    bridge.py        async-to-sync bridge thread
    manager.py       MCPManager — boots stdio servers, registers tools
    servers.py       MCPServer dataclass + JSON loader
  memory.py          MemoryStore with .attic/ soft-delete
  profiles.py        partial TOML overlay merge
  project.py         find_project_file → ProjectContext
  recommend.py       tiered model picks per VRAM
  scheduled.py       ScheduledTask + TaskStore
  scheduler.py       Scheduler — APScheduler wrapper
  skills.py          SkillStore + minimal frontmatter parser
  tools/
    base.py          @tool decorator + REGISTRY + Tool dataclass
    code.py          run_python tool (subprocess)
    computer.py      pyautogui wrappers — screenshot/click/type/key/scroll
    fs.py            read_file/write_file/list_dir
    image_comfy.py   ComfyUI text2img
    memory.py        remember/recall/forget/list_memories
    skills.py        list_skills/invoke_skill
    subagent.py      delegate_explore/delegate_plan
    voice.py         speak_text/transcribe_microphone
    websearch.py     web_search/web_fetch
  transcripts.py     JSONL session log store
  util/streaming.py  legacy
  voice.py           TTS (platform CLIs) + STT (faster-whisper)
  worktree.py        git worktree wrapper
  apps/              frontends (shipped in the wheel)
    cli/main.py      Typer CLI — every public command
    web/server.py    FastAPI + SSE + permission flow
    web/static/      vanilla JS chat UI
desktop/             Tauri 2 project (NOT a Python package)
tests/               pytest — one file per module
docs/                this folder
scripts/             install + dev helpers

Adding a tool

  1. Create evi/tools/<thing>.py:
   from evi.tools.base import tool

   @tool(
       description="What this tool does for the model.",
       category="thing",  # also: the config toggle name
   )
   def the_tool(arg1: str, arg2: int = 5) -> str:
       ...
       return "result"
  1. Add the category to ToolToggles in evi/config.py.
  2. Import for side effect in:
    • evi/apps/cli/main.py
    • evi/apps/web/server.py
  3. Decide whether the category belongs in auto.auto_approve. If it does anything irreversible or network-y, leave it out.
  4. Test pattern:
   from evi.tools.base import REGISTRY
   import evi.tools.thing  # noqa: F401  register

   def test_the_tool() -> None:
       out = REGISTRY["the_tool"].call(json.dumps({"arg1": "x"}))
       assert ...

Adding a backend

evi/backends/__init__.py registers backends in KNOWN_BACKENDS. Each subclass of Backend:

Add a default port to _DEFAULT_URLS in factory.py. Add tests in tests/test_backends.py (use httpx.MockTransport).

Adding a slash command

Built-in commands live in evi/apps/cli/main.py and evi/apps/web/server.py. The two paths are intentionally separate:

User-defined commands need no code — they're just markdown files in ~/.evi/commands/.

Adding a subagent profile

Add an entry to SUBAGENT_PROFILES in evi/llm/subagent.py:

SUBAGENT_PROFILES["reviewer"] = {
    "system_prompt": "You review pull requests for correctness…",
    "tool_categories": ("fs",),
}

Then add a delegate tool in evi/tools/subagent.py that wraps run_subagent(...) with that profile.

Common gotchas

Bumping a phase

When you add a meaningful chunk of work:

  1. Add tests covering the new code paths.
  2. Update README.md "What's built" table.
  3. Update docs/architecture.md if you added a new subsystem.
  4. Add a memory entry in ~/.claude/projects/C--evi/memory/project_evi.md so the next Claude Code session knows what changed.

Releasing

Nothing automated yet. Planned: scripts/release.sh that runs pytest && ruff check && python -m build, tags the commit, attaches wheels.

Edit on GitHub ↗