𧬠Also the LLM plane of ml-platform β and still standalone
This repository's core, use-case, eval harness and twelve ADRs were vendored with their git history into the
ml-platformmonorepo, where the agent core becamelibs/llm-coreand the store assistant becameprojects/store-assistant.This repository was not archived, and stays live β as a distribution.
ml-platform'slibs/llm-coreis authoritative for the agent core (platform-ADR-010), andcore/here is a one-way export of it: pinned to a source commit incore/EXPORTED_FROM.json, with a hash of every file. CI re-runs the export at that commit and fails on any difference, so a hand edit cannot pass by updating the hashes too. This repository is how to take the agent core without the platform around it.So changes to
core/go to ml-platform, and arrive here on the next export. Everything outsidecore/β thetiendause-case, the app, the evaluation harness β is this repository's own.Why the code was consolidated. This repository carried an explicit cross-repository contract with the sibling template β two CI configurations, two changelogs, two ADR sets, two coherence systems, and a shared plan living in one repository while governing another. A third participant would have made that coordination cost superlinear. Consolidation removes the coordination rather than optimising it. The reasoning, and a dated Correction reversing the original decision to archive this repository, are both in ml-platform ADR-002.
The commits that closed this repository's first phase are worth reading on their own: ADR-011 and ADR-012 re-derived the tier topology from measured hardware, and ADR-012 carries a same-day Correction documenting two wrong claims β a budget taken from a single reading of a fluctuating quantity, and a model rejected on a benchmark that had been run under the assumption it was cited to justify. Both are preserved rather than edited away. That episode is the founding evidence for
ml-platform's verification rules.
A business-agnostic, multi-tier local LLM agent core that teams can adopt
across domains. The valuable logic β grammar-constrained routing, an adaptive
reasoning loop, objective escalation and a deterministic policy gate β lives in
a reusable core/. A new domain is a thin usecases/<name>/ folder, never a
fork of the core (see ADR-001).
The shipped example use-case, tienda, is a WhatsApp store assistant.
This repository is the LLM plane of a deliberately connected line of work:
- ML-MLOps Portfolio β three production ML services; the lessons were paid for here.
- ml-service-template β those lessons encoded as a governed scaffold for classical ML on Kubernetes (scikit-learn, XGBoost, LightGBM; single team, 1β5 models).
agent-local(this repo) β the same governance philosophy (AUTO/CONSULT/STOP, eval-gated autonomy, policy-as-data, no fine-tuning yet) generalized to a new domain: local LLM agents.- ml-platform β the multi-project substrate above the template's boundary, which vendored this repository's core in as
libs/llm-coreand its use-case asprojects/store-assistantβ and which is now authoritative for that core.The cross-repository contract described here previously was dissolved, not optimised.
ml-platformplatform-ADR-002 consolidated the code precisely so that no plan document in one repository governs another. What remains is one-way, and it runs from the platform: the core is developed there and exported here. It stays business-agnostic because the platform enforces it β its library may not import a use-case, so the exported core resolves none by name, andbuild_agent(load_usecase(root), registry)is wired by the caller. See ADR-001 for why that agnosticism is the property worth protecting.
Status: Phase 1 (read-only, fixtures). Routing quality gate PASSED (20/20) on the Tier-0 router β an initial 19/20 was re-scored after
bench/RESULTS.mdfound the router right and the gold label wrong. Code is structured for the full multi-tier stack.
Most "LLM agent" code couples the loop, prompts and business rules into one app. That doesn't scale to multiple use-cases: the safety-critical logic diverges across copies. Here, that logic is centralized and consumed by configuration:
core/ # business-agnostic engine β single source of truth
config.py # UsecaseConfig loader
schemas.py # typed Pydantic contracts
router.py # Tier-0 router (GBNF-constrained JSON)
tiers.py # tier clients (endpoints injected from config)
tools.py # ToolRegistry (per-use-case namespaces)
retrieval.py # BM25 index + semantic_retrieval factory
policy.py # deterministic policy gate (rules are data)
agent.py # the 7-station loop
__init__.py # load_agent(name)
usecases/
tienda/ # example use-case (config + tools + data + prompts + evals)
config.yaml # endpoints, allowed_intents, policy rules, prompt templates
tools.py # build_registry(config) -> ToolRegistry
prompts/ grammars/ data/ policies/ budgets.yaml evals/sets/
app/
main.py # FastAPI surface; loads a use-case via AGENT_USECASE
Customer ββΆ FastAPI ββΆ Agent.handle()
β
1. route (Tier 0, GBNF) β intent / tier / risk / confidence
2. plan (Tier N) β list of tool calls
3. tools (APP executes) β observations
4. reflect (conditional) β only on tool-failure or risk β₯ medium
5. generate (Tier N) β draft answer
6. critic (Tier N/N+1) β verify against observations (risk β₯ medium)
7. policy (deterministic) β MANDATORY gate; no response bypasses it
8. finalize β answer + metrics
Adaptive depth: simple smalltalk goes plan β tools β policy β final
without paying for reflection/critique.
Objective escalation (in code, never in the prompt): confidence < 0.70
bumps a tier; a critic rejection bumps once; Tier-3 requires explicit budget
permission.
- Python 3.11+
- For the default profile: a llama.cpp
llama-serverbuild and a GGUF router model (Tier 0). Roughly 2 GB of RAM β one model, not four.
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]" # or: pip install -r requirements-dev.txtResident memory β not token cost β is the binding constraint on a workstation, so Tier 0 (small, hot, grammar-constrained) stays local while the large, rarely-called tiers go remote. One committed config serves all three profiles; the environment picks which is active.
| Profile | How to select it | Resident RAM |
|---|---|---|
local-only |
export nothing β higher tiers are dropped and every escalation resolves down to Tier 0 | ~2 GB |
hybrid |
export AGENT_TIER{1,2,3}_URL / _MODEL / _API_KEY |
~2 GB |
all-remote |
additionally set AGENT_TIER0_URL + AGENT_TIER0_KIND=remote |
~0 GB |
Credentials are named, never inlined: the config references a variable name,
so it can be committed. See .env.example for every variable.
all-remoteloses GBNF grammar constraints on routing (a llama.cpp capability) and falls back toresponse_format: json_object. It is meant for CI, not for the quality-gated path β the routing accuracy gate is only meaningful underlocal-onlyorhybrid.
llama-server -m /path/to/router-model.gguf --port 8091 -ngl 99 -c 8192 --host 127.0.0.1# Tests (no model required)
pytest
# Routing eval (gate: >= 18/20 intent accuracy)
python evals/run.py 01_intent.jsonl --usecase tienda
# Dev API
AGENT_USECASE=tienda python -m app.main
curl -X POST http://localhost:8000/dev/message \
-H "Content-Type: application/json" \
-d '{"text": "tienen coca de 600 fria?"}'cp .env.example .env # set MODELS_DIR to your host model directory
docker compose up --buildModels are mounted as a read-only volume β never baked into the image.
usecases/<name>/
βββ __init__.py # from .tools import build_registry
βββ config.yaml # endpoints, allowed_intents, policy rules, prompts
βββ prompts/router.md
βββ grammars/route.gbnf
βββ tools.py # build_registry(config) -> ToolRegistry
βββ data/ # fixtures (Phase 1) or API clients (Phase 2)
βββ policies/*.md # BM25-indexed docs
βββ budgets.yaml
βββ evals/sets/*.jsonlThen: AGENT_USECASE=<name> python -m app.main. See the full authoring guide
docs/usecases.md (contract, consumption modes,
bring-your-own-models) and CONTRIBUTING.md.
| Phase | Gate | Status |
|---|---|---|
| F0 | Tier-0 router speed β₯ 25 tok/s | β
(see bench/RESULTS.md) |
| F1 | Routing intent accuracy β₯ 18/20 | β 20/20 |
| F1 | All tools read-only (order_create dry-run) |
β |
| F1 | Deterministic policy gate enforced | β |
| F2.0 | ExecutiveController + per-tier circuit breaker | β |
| F2.0 | Tier-client retry/backoff (transient blips β tier failure) | β |
| F1.6 | Latency-budget enforced (safe degrade past deadline) | β |
| F2.0 | Resident-memory invariant enforced at config load (ADR-011) | β |
| F2.0 | Suite runs with no model and no credentials (local-only) |
β 156 passed |
- No fine-tuning at this stage β routing + prompts + retrieval.
- The model never mutates critical state without the policy gate β enforced structurally by the fail-closed tool capability contract (ADR-006).
- Every lane needs an eval harness before increasing autonomy.
- The simplest loop that works.
- Inventory/price/stock are never held in model memory β always live tools.
- Local-first; cloud only as explicit, budgeted overflow. "Budgeted" is
literal β remote tiers are opt-in per tier, capped by
budgets.yaml, and the number of models allowed to occupy RAM is an enforced config invariant (ADR-011).
- Phase 1 β Skeleton β (this): core + use-case, routing gate, policy gate, Docker.
- Phase 2 β executive controller, versioned YAML policies, verifier pass, 10 eval sets, SQLite queue + sagas for multi-day flows.
- Phase 3 β telemetry (PII-redacted), shadow mode, retrieval growth loop.
- Phase 4 β QLoRA (strategic gate; requires β₯4 weeks of logs + a new ADR).
- ADR-001 β reusable platform, not a copy template
- ADR-002 β calibrated infrastructure
- ADR-003 β policy rules as versioned data
- ADR-004 β cross-tier verification
- ADR-005 β decision telemetry as a contract
- ADR-006 β fail-closed tool capability contract
- ADR-007 β structured tool-calling contract
- ADR-008 β retrieval/tier surface is caller-isolated, not server-isolated
- ADR-009 β reflection output is a notes channel, never an observation
- ADR-010 β MCP / A2A interoperability: Rejected (with revisit triggers)
- ADR-011 β hybrid tier topology: resident memory is the binding constraint
- ADR-012 β "local" is two budgets, not one: device-aware memory invariant
- docs/SECURITY_MODEL.md β OWASP Top 10 for LLM Applications control-by-control map
- docs/workstation-memory-budget.md β running the platform on a memory-constrained machine (ADR-011 in practice)
- CHANGELOG.md β version history
- CONTRIBUTING.md β dev setup, adding use-cases, quality gates
- SECURITY.md β vulnerability reporting process (see
docs/SECURITY_MODEL.mdfor the OWASP threat-model mapping) bench/RESULTS.mdβ benchmark + routing gate evidence