Tuffy's agent core. Nothing in here prints to a terminal or reads stdin — that's src/cli/'s
job and main.py's alone. Every other package is usable headless.
| Path | What it owns |
|---|---|
| engine/ | The ReAct loop itself, provider-agnostic: turn_engine.run_turn() drives Thought → <tool_call> → Observation → ... as one flat generator of typed TurnEvents (see engine/README.md if present, or the module docstrings). Replaces the old LocalAgent/agent.py design of live-yielded tokens + callbacks. |
| identity.py | The agent's fixed self-model (name, capabilities) — never LLM-written, never stored as "memory". |
| memory.py | Thin adapter wrapping the external Elastimem library (SQLite-backed facts/episodic/lessons store): the remember/recall tools, attach_llm()/reconfigure_for_model() wiring, and clear_memory() (backs the /purge command). |
| embedder.py | First-run setup for Elastimem's built-in semantic-recall embedder (bge-small via fastembed): durable cache location, silenced Hugging Face console output, an announced prewarm at startup (instead of a lazy ~67MB download inside the first turn), and a hard no-network gate in offline mode. |
| settings.py | Persisted user settings (.tuffy/settings.json, gitignored) — currently just the chosen default model id, set via /models default <id>. |
| vision.py | Image encoding (file → data URI) and the IMAGE_SENTINEL protocol tools use to hand an image back to the turn engine. |
| cli/ | The interactive terminal chat: banner, spinner, slash commands, turn loop (turn.py drives engine.turn_engine.run_turn and renders each event). Only main.py imports this. |
| llm/ | LLMProvider interface + adapters (llama_cpp, openai_compatible) — how the engine talks to a model without hardcoding a backend. API failures (rate limits, bad keys, network errors) surface as ProviderError, caught by src/cli/turn.py so the session survives. |
| models/ | ModelRegistry — model cards, load params, sampling params, rate-limit metadata. configs/local.py and configs/api.py hold the actual model card definitions; weights/ holds gitignored GGUF files. |
| prompts/ | Every system-prompt string, centralized: personas.yaml (static tone/rules) + templates.py (Python-built fragments). |
| tools/ | Native tools the agent can call, grouped by domain, plus the MCP client that registers external servers' tools the same way. |
| skills/ | Discovery/loading mechanism for ./.tuffy/skills/*/ capability packs (content lives at the repo root, not here). |
| voice/ | Voice CLI layer: local Speech-to-Text (Whisper) and Text-to-Speech (Piper) wrappers + interactive audio loop; weights/ holds gitignored voice model weight files. |
API-provider models read their key from an environment variable named by the model card's
api_key_env (see llm/README.md). If it's not exported in your shell,
the provider falls back to reading that one key out of a .env file at the repo root — a real
exported env var always wins. .env is gitignored, so keys placed there are never committed.
src/cli/turn.pybuilds the system prompt fresh (session.system_message(), backed bysrc/prompts.build_system_prompt) from: identity, tool signatures, long-term memory (an Elastimem context plan), session summaries, lessons, and installed skills.- The user's message goes into
src.engine.turn_engine.run_turn()— a ReAct loop:Thought → <tool_call> JSON → Observation → ... → final text answer, capped at a fixed hop budget, expressed as one flat generator of typed events (Status,Thought,ToolCall,ToolResult,Token,Done,Failed) thatsrc/cli/turn.pyrenders as they arrive. - Tool calls are parsed by
src.engine.tool_dispatch, dispatched throughsrc.tools.registry, and the result fed back as a synthetic turn. - After the turn, Elastimem (via
src/memory.py'sattach_llm()-wired background worker) runs fact extraction and session summarization to record durable facts about the user (never about the agent itself) — see data/README.md for the memory-tier details.
See ARCHITECTURE.md at the repo root for the full HLD/LLD design rationale, including diagrams of the request flow and how each component's contract fits together.