Metadata-Version: 2.4
Name: avalon-ai
Version: 0.1.1
Summary: Avalon: an adaptive AI runtime — heartbeat, hot-swappable LoRA skills, nightly consolidation, and a fitness gate. v1 slice.
Author: Lunar Labs
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Requires-Dist: lunarlabs-core==0.1.0
Requires-Dist: z3-solver>=4.12
Requires-Dist: regex>=2024.5.10
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Provides-Extra: mlx
Requires-Dist: mlx-lm>=0.26; extra == "mlx"
Dynamic: license-file

# Avalon

An adaptive AI runtime. MIT © Lunar Labs.

The LLM is a stateless function `f(context) -> token`. Avalon is the runtime
around it that persists, perceives continuously, holds state, learns while
running, and acts on a clock — the model is demoted to a frozen language
cortex; all learning lives in hot-swappable LoRA adapters gated by measured
fitness.

This is **v1**: the first slice of a much larger vision. Every module maps
to a constraint measured on real hardware (Mac Mini M4 / MacBook M1 Pro,
Qwen3-4B 4-bit, MLX, 2026-07). Where the research hit a wall, v1 ships an
honest interface instead of a fake.

## Architecture

```
                 external events (sensors, messages, tools)
                              │  land whether or not anyone is talking
                              ▼
   ┌──────────────────────────────────────────────────────────┐
   │ HEARTBEAT  (intrinsic time)                              │
   │ tick loop · nested clocks now/day/project/life           │
   │ ONE append-only event stream · atomic JSON state         │
   │ daemon mode: avalon serve · intake: avalon inject        │
   └──────────────────────────────────────────────────────────┘
                              │
                              ▼
   ┌──────────────────────────────────────────────────────────┐
   │ DEEP TIME  (memory with half-lives)                      │
   │ salience decays exponentially (36 h default) · surprise  │
   │ events RE-INK referenced memories · event stream ->      │
   │ episodes · learned circadian histogram (starts blank)    │
   └──────────────────────────────────────────────────────────┘
                              │
                              ▼
   ┌──────────────────────────────────────────────────────────┐
   │ EPISODIC BUFFER  (provenance-typed, surprise-gated)      │
   │ observed │ inferred │ said-by-assistant                  │
   │ ONLY observed is trainable — anti-fabrication defense    │
   └──────────────────────────────────────────────────────────┘
                              │ observed entries, retrieval-task Q→A
                              ▼            (NEVER transcripts)
   ┌──────────────────────────────────────────────────────────┐
   │ CONSOLIDATION  (sleep)                    per night      │
   │ sufficiency gate → 2:1 mandatory rehearsal → collapse    │
   │ cap → training manifest → pluggable Trainer              │
   └──────────────────────────────────────────────────────────┘
                              │ candidate adapter (one file)
                              ▼
   ┌──────────────────────────────────────────────────────────┐
   │ FITNESS GATE  (git for a mind)                           │
   │ probe fixed held-out set ± adapter → DIFF = what it      │
   │ learned → regression check FIRST → promote | roll back   │
   └──────────────────────────────────────────────────────────┘
                              │ promoted
                              ▼
   ┌──────────────────────────────────────────────────────────┐
   │ ADAPTER REGISTRY  (the registry IS the tool list)        │
   │ named callable skills · base-version pin · fitness score │
   │ ONE live adapter · 2-3 hot pre-fetched · 5 ms swap       │
   └──────────────────────────────────────────────────────────┘
                              │ adapter_path per request
                              ▼
   ┌──────────────────────────────────────────────────────────┐
   │ FROZEN BASE  (OpenAI-compatible server, mlx_lm style)    │
   │ never trains · auto-launched local mlx_lm server on a    │
   │ free port (pilot-run), or your own base_url              │
   └──────────────────────────────────────────────────────────┘
```

## Measured operating points

These are bench numbers, not defaults pulled from thin air. They live in
`avalon/config.py` with citations in comments.

| Constraint | Measured value | Where it's enforced |
|---|---|---|
| Consolidation point | **lr 5e-5, 48 iters** — 3/3 recall after full context wipe; 8 iters = degenerate loops | `config.py`, pinned in every manifest |
| Paraphrases per fact | **~10** (27 rows about one fact = mode collapse) | `consolidate.cap_rows_per_fact` |
| Rehearsal | **2:1 mandatory** — 0:1 → 0/3 prior facts survive; 2:1 → 3/3 | `consolidate.build_manifest` |
| Training shape | **Q→A retrieval task** — transcripts scored 0/3 on a perfect loss curve | `consolidate.pair_from_entry`, lunarlabs-core `TrainingPair` |
| Sufficiency | **skip empty days** — updates on signal, not schedule | `build_manifest` sufficiency gate |
| Live adapters | **ONE** — multi-adapter composition costs ~40% each | `registry.activate` |
| Adapter swap | **5 ms** attach; pre-fetch 2-3 hot | `registry` hot cache + swap latency |
| Adapter size | rank-16, 8 layers = 14.7 MB | `config.py` |
| Trainable memory | **observed only** — inferred / said-by-assistant never train | `buffer.trainable()` |
| Memory half-life | **36 h default** — salience = surprise × strength × 0.5^(age/half-life); re-inking resets the clock | `deeptime.effective_salience`, `buffer.reink` |
| Episodes | **30 min silence** closes an episode; consolidation reads episodes, not a flat day-file | `deeptime.segment_episodes` |
| Circadian | **learned histogram** — 24 hour buckets fed by ticks/events; uniform until lived | `heartbeat.hour_histogram`, `deeptime.expected_activity` |
| Promotion | **behavioral gate** — loss lies at 48 iters (fact blending looked perfect) | `gate.run_gate` |

## Run the public proof

Requirements: an Apple-silicon Mac, Python 3.10 or newer, git on your path (for
a one-time dependency fetch; run `git --version` and install the Xcode command
line tools if macOS prompts you), about 4 GB of free disk space, and an internet
connection for the first run. The first run
downloads a roughly 2 GB model once. Later runs reuse it. Training time depends
on the Mac and may take a few minutes.

```bash
pip install "avalon-ai[mlx]"
avalon demo
```

The command runs two proofs with one local 4-bit model:

1. It teaches five invented toy facts, starts fresh answering processes whose
   prompts contain only the questions, and measures recall from the adapter.
2. It gives the unguarded model a poisoned toy note, then asks Avalon's
   witnessed-fact gate the same question. With no witnessed fact, the gate
   answers `I do not know.` and the model is called zero times.

No API key is needed. No cloud model receives the prompts. The only network use
is the one-time public model download. Demo files stay in `.avalon/demo_run`.
Exit code `0` means all five trained questions survived the context wipe and
the poison gate refused with zero model calls. Exit code `1` means the proof or
runtime failed. Exit code `2` means the machine cannot run MLX.

If startup reports that MLX cannot use Metal, run the command from a normal
Terminal rather than a headless or virtualized session. If the model download
is interrupted, reconnect and rerun the command. The download resumes and is
reused.

The latest isolated-environment terminal capture is in
[`launch/clean-run-output.txt`](launch/clean-run-output.txt). It records 5/5
trained recall, 5/5 unseen-phrasing recall, and poison refusal with zero model
calls. Its header states the one untested first-run condition: the model was
already present in this Mac's shared Hugging Face cache.

## Use it on your own facts

Turn your own structured config into witnessed facts, arm the gate, and ask:

```bash
avalon ingest-facts ./chatterbox.json        # dry-run first: shows every candidate, mints nothing
avalon ingest-facts --commit ./chatterbox.json
avalon facts-gate on
avalon answer "what port is chatterbox on"   # -> 8100 (witnessed: chatterbox.json:12).
avalon answer "what is the door code"        # -> refusal: I do not know.  (model calls: 0)
```

What the gate guarantees, stated plainly:

- It answers **only** from facts you minted, each carrying a verbatim source
  line. The value it returns is the witnessed value — never a guess.
- On a fact-shaped question whose **key it cannot find among your witnessed
  facts**, it refuses with `I do not know.` and **the model is never invoked**
  (zero model calls — there is no channel for it to confabulate into). That is
  the anti-fabrication guarantee, and it is the honest boundary of the claim:
  the gate refuses on a *witnessed key it cannot find*, not on every possible
  phrasing. A question it cannot key (e.g. `chatterbox port?`) is not a fact
  query and falls through to the normal chain.
- A witnessed value still passes your send-time constraints (P09): a rule like
  "never reveal the door code" blocks even a witnessed value.
- Non-fact questions go to the **normal draft + coverage chain**, which needs a
  model backend — `--backend resident` reuses the demo's local model, or point
  `base_url` at a server you run. A buyer with no model server still gets the
  canned witnessed value on fact queries (the refusal and the canned value need
  no model at all).

The gate defaults **off** — `avalon answer` behaves exactly as before until you
turn it on. `avalon facts-gate status` prints the resolved verdict (so a config
typo is visible) and how many facts you have minted (a flag with no facts still
falls through — flags are not reachability). `avalon facts-gate off` disarms it.

## Daily use on your own data (real training)

One command a night, on your own notes, with **real** learning — no server to
set up, no mock, no silent hang. Apple silicon + the `mlx` extra:

```bash
pip install "avalon-ai[mlx]"

# 1. drop tonight's facts in (a .txt / .md / .jsonl file, or inject one line)
avalon ingest notes ~/notes/today.md
avalon inject "the Northstar cache listens on port 49321"

# 2. run the night — this trains for real and proves it
avalon pilot-run --ask "What port is the Northstar cache on?"
```

What `pilot-run` does, in order (each stage prints a wall-clock line):

1. **Preflight** — Apple silicon, `mlx_lm` importable, Metal usable, ≥ 4 GB
   free. It stops here (exit 2) *before any download* if the machine can't run.
2. **Intake** — your notes become `OBSERVED` facts (re-running the same file is
   a no-op; nothing is trained twice).
3. **Extract** — turns notes into Q/A pairs through a model server it
   **auto-launches** for you on a free port (first run downloads the model once,
   ~2.5 GB), then shuts it down before training. `--no-extract` trains the notes
   as-is with no server.
4. **Sleep** — one real LoRA night (the shipped `MaskingMLXTrainer` + a resident
   gate that actually attaches the candidate adapter).
5. **Verify** — the adapter on disk must be a real (> 1 MB) LoRA, or you get
   `TRAINING DID NOT PRODUCE A REAL ADAPTER` and exit 1. No stub ever counts.
6. **Morning proof** — the base model's answer vs the live-adapter answer to
   each `--ask`, printed verbatim, so you can *see* the weights changed.

`--nights N` loops it; a night with no new facts is a valid **skip** (exit 0) —
updates happen on signal, not on a schedule. Roughly 3–6 min per night on an
M-series Mac after the one-time download.

Exit codes: **0** promoted or validly skipped · **1** rolled back / stub
adapter · **2** this Mac can't run it · **3** the model server couldn't start ·
**130** Ctrl-C (the child server is killed).

**Privacy:** nothing leaves your Mac except the one-time public model download.
Your notes train into local weights and stay there.

**`pilot-run` never uses `MockTrainer`.** `config.trainer_backend` (the safe
default for every *other* command) is ignored here — this command always trains
for real, by construction.

Optional: `deploy/ai.lunarlabs.avalon.pilot.plist` runs `pilot-run` nightly at
03:00 with logs under `<home>/logs/pilot.log`.

## No-GPU smoke test (FAKES learning, CI only)

```bash
python3 -m venv .venv
.venv/bin/pip install -e ".[dev,mlx]"   # lunarlabs-core comes from git; mlx extra powers the live demo
.venv/bin/pytest                    # no GPU, no network
.venv/bin/avalon tick
.venv/bin/avalon serve --interval 60   # daemon mode; SIGTERM stops it cleanly
.venv/bin/avalon inject "the deploy moved to friday"   # event -> buffer bridge
.venv/bin/avalon consolidate --dry-run
.venv/bin/avalon status
```

State lives in `./.avalon` (override with `--home`). The default trainer hook
for `serve` / `consolidate` is **`MockTrainer`**, which writes a stub
`.safetensors` and **learns nothing** — it exists for CI and plumbing checks
only. **Never use it to evaluate Avalon; use `avalon pilot-run`, which always
trains for real.** On a GPU host, `consolidate --mlx` plugs in `LocalMLXTrainer`
(shells to `mlx_lm lora`). The default model backend is `FrozenBaseClient`;
tests use `MockModel` — no GPU in tests, ever.

## The organs (P01–P22)

Every research proposal in the program now exists as a module: an interface
matching the measured constraint, a PROVISIONAL default where the real
signal isn't built yet, and the proposal's falsifier as runnable code whose
kill condition exits nonzero. The full map from inventory item to module,
status, and falsifier price is `docs/the-organism.md`.

`bpc` (P12, the north-star eval — first real run: `docs/p12-first-run.md`),
`wagers` (P06), `coverage` (P05, gate 3), `nightindex` (P03), `vetoes`
(P02), `constraints` (P09), `retraction` (P10), `changes` (P18),
`timeweight` (P15), `flywheel` (P01+P04), `ledger` (P07), `masking` (P08),
`dreaming` (P11), `skills` (P13), `physio` (P14), `perception` (P16),
`stratify` (P17), `river` (P19), `cortex` (P21), `middlememory` (P22),
plus `tools/do-ceiling/` (P20, gate 2 kit).

## What is still a wall

Stated plainly — the organs build interfaces around these, not over them:

- **The real surprise signal** — the MECHANISM now ships in Avalon:
  `predictive_surprise.PredictiveSurpriseScorer` scores surprise as the live
  adapter's measured BPC prediction error on incoming observed text (DEFAULT
  OFF until `avalon predict-latency` prices the per-event forward pass). What
  stays JARVIS-side is the owner corpus, the owner model, and the felt-present
  tuning — not the organ. The other token-overlap heuristics (wager resolver,
  ledger matcher, change join) remain PROVISIONAL behind their Protocols.
- **Implicit adapter routing** — untested; `NoRouter` abstains, embedding
  similarity is the named candidate.
- **P12 first run (2026-08-04)** — all three adapters LOSE to the frozen
  base on future-Carter BPC: a uniform style-drift tax (~+0.23) and zero
  future-specific gain. The adaptive thesis is unproven where it counts;
  the instrument to fix that now exists with baselines.
- **Constraint application in weights** — recites 3/3, applies 2/3; the
  runtime verifier tier (`constraints.py`) is the live answer.
- **Surgical forgetting** — 3 methods failed; the organ is replay-with-
  exclusion + periodic rebase (`retraction.py`), not subtraction.
- **Subjective time / anticipation gradients** — still research; deep time
  remains half-lives + episodes + circadian histogram.
- **Base-upgrade retraining bill** — pins enforced; `cortex.SwapBill`
  prices it, the number is still an estimate.
- **Real training in CI** — `LocalMLXTrainer` and `MLXScorer` need a GPU
  host; the suite exercises contracts via mocks only.
