Imported from prabhjot0109/sentient (
AGENTS.md). Install upstream withnpx skills add prabhjot0109/sentient. Copyright stays with the author.
AGENTS.md
Guidance for Codex when working in this repository.
What Sentient is
A RAG backend for AI-driven NPC dialogue in games. Today it serves the Mantella Skyrim mod through
an OpenAI-compatible /v1/chat/completions adapter (FastAPI + LangChain, FAISS local index,
multi-provider LLMs). It is mid-refactor into a multi-project AI runtime: one deployment, many
users, many game "projects" — each project a database row owning its config, one editable
persona (per game, never per NPC), documents, and chat threads. Start at
docs/superpowers/plans/order.md — it is the single source of truth for what is done and what is
next; the architecture lives in
docs/superpowers/specs/2026-07-07-sentient-world-runtime-overview.md and the backlog in
docs/superpowers/plans/2026-08-11-post-R8-launch-todo.md.
Plans 01–03, R1–R7, R8, V1–V6, Phase F, Phase S, Phase D, Phase P, and post-deploy engineering (PgVector, LangChain 1.x, retrieval evals, presets) are all done on main.
Sentient is deployed and serving in production (Render Singapore, Neon Postgres, Qdrant Cloud, Vercel console & landing).
Test counts: The suite collects 575 pytest tests and 155 console vitest cases as of 2026-09-06. Each plan file also carries 🔶 DELTA banners that override its body text, newest delta wins. Never copy a test count, file list, or "state at time of writing" line out of a plan — regenerate it, and confirm a referenced file exists before relying on it.
Commands
uv run python -m pytest tests/ -v # full test suite (the DoD gate for every plan task)
uv run python -m pytest tests/test_qdrant_backend.py -v # one file
uv run uvicorn sentient.api.app:app --port 8000 # run the API
uv run ruff check src tests # lint
uv run ruff format --check src tests # format gate
uv run mypy src/sentient/core src/sentient/adapters # strict typing on the Protocol seams
uv run lint-imports # the layer contract
uv add <package> # deps via uv only; never hand-edit uv.lock
CI (.github/workflows/ci.yml) runs all five on every push and pull request.
Architecture — four layers, one direction
Dependencies point one way only: api → services → adapters → core. import-linter enforces
it; a violation is an architectural regression, so move the code rather than weaken the contract.
core/— no I/O, no framework.config.pyis the ONLY place env vars are read (frozenRAGSettingsviaload_rag_settings(); every new knob goes here with the_env_int/_env_float/_env_boolhelpers).errors.pyholds the HTTP-free domain exception hierarchy routers translate to status codes. Alsocache.py(ObjectRegistrysingle-flight client cache),concurrency.py(IngestJob,IngestQueue,SessionLocks,defer— in-process, not crash-durable),crypto.py(Fernet vault primitives),presets.py.adapters/— everything that talks to the outside world.state/is the asyncStateStoreProtocol (base.py) withSQLiteStateStore(default,data/state.db) andPostgresStateStore(asyncpg; Neon and Supabase — only the DSN differs); it owns users, api_keys, projects, project_configs, documents, provider_credentials, chat_threads, chat_messages.retrieval/is the asyncVectorBackendProtocol withFaissBackend(default) andQdrantBackend(hybrid dense+sparse BM25/RRF, SQ8, tenant/project payload filters), selected byfactory.pyonVECTOR_BACKEND.llm/holdsmodels.py(provider client construction),openai_wire.py(wire translation + SSE) andpersona.py.documents.pyisArchivesIngestion(PDF/TXT load, split, OCR).auth.pyis API-key hashing + JWT/JWKS identity +IdentityCache.stt/is provider resolution and the pure numpy/waveWAV diagnostics — its thresholds are fitted to real captures, so do not retune them without new measurements.services/— domain logic, no FastAPI.rag.py(NPCBrain),runtime.py(RuntimeContextper-request tenant resolution,embedding_signature,RuntimeCache),chat.py,ingestion.py,projects.py,credentials.py,transcription.py,condense.py,memory.py.api/— HTTP only.app.pyis a 112-line app factory: lifespan, CORS, nineinclude_routercalls, nothing else.deps.pyis the composition root holding every module-level singleton.routers/holds the nine routers (none over 250 lines) andschemas/the Pydantic bodies.
apps/ holds the two Node apps, both outside every backend gate (ruff/mypy/import-linter/
pytest are path-scoped to src/ and tests/, so nothing there can turn the Python CI red):
apps/console/ is the console, and apps/landing/ is the marketing site, whose Launch CTA is
a plain cross-origin link to the console's /auth/sign-in — which is what lets the two stay
separate builds with no cross-origin token handoff. Each has its own AGENTS.md,
package.json and npm lockfile; there is no workspace tool (D5). Each also has its own
GitHub Actions workflow (.github/workflows/console.yml, landing.yml), path-filtered and
separate from ci.yml so a frontend failure never reads as "the backend is broken".
apps/web/, the frozen pre-refactor test UI, was deleted by F12. It had stopped being a
reference and become a fossil that gave wrong answers: it called /v1/chats*, which R9
removed, and sent no Authorization header on any request, so it 401'd on everything after
E3. Its shapes are all covered by apps/console/src/lib/api/.
cli.py is the sentient console script. migrations/ holds the .sql files
PostgresStateStore applies on first connect. config/config.ini is Mantella's config, checked
in as reference wiring only — Sentient never reads it; Mantella reads
Documents/My Games/Mantella/config.ini. tests/ is unittest.TestCase /
IsolatedAsyncioTestCase under a pytest runner.
Hard constraints (from the runtime overview §8 — apply to all new code)
-
Routers import the deps MODULE, never names out of it (spec §7.1):
from sentient.api import deps # correct deps.state_store # resolved at call time, so patching works from sentient.api.deps import state_store # WRONGThe name form binds the value at import time, so a test patching
deps.state_storewould have no effect and the router would keep using the real store — a test passing for the wrong reason. No type checker or linter catches this;tests/test_layer_rule.pypins it instead. -
Singletons stay module-level in
deps.py(spec §7.2). Do not convert them to FastAPIdependency_overrides. -
Async-native: no blocking I/O on the event loop. Sync CPU work goes through
asyncio.to_thread;asyncio.Lockis acquired at coroutine level, never held across ato_thread, and cached per event loop (see_get_index_lockinfaiss_store.py). -
Defaults preserve behavior:
VECTOR_BACKEND=faiss, noDATABASE_URL⇒ SQLite, flags off. A fresh clone must behave likemain. Back-compat on existing endpoints is a hard constraint. -
No network in tests: Qdrant uses
location=":memory:", fake embeddings, patched clients. -
Clients connect over
127.0.0.1, neverlocalhost. Uvicorn binds IPv4 only and Windows resolveslocalhostto::1first: measured 208 ms of wasted connect time per request, invisible in server-side logs. See README "Latency: what actually matters". -
Python ≥3.12, LangChain <1.0.0. TDD per task: failing test → green → commit (
feat:/refactor:/test:/docs:/chore:prefixes, one commit per task). -
.env.example+README.mdupdated in the same change that adds a flag.
Working on the refactor
Read plans/order.md first, then the specific plan file — each carries 🔶 DELTA banners that
override its body text; the newest delta wins. Personas are per-project
(project_configs.persona_prompt): do not reintroduce per-NPC persona tables or npc_name-keyed
config resolution. docs/ is deliberately local-only and is ignored by git (.gitignore has
/docs), so git add -A is safe and plan files never enter a commit.