Stacks (hal0 docs)

Editing slots one at a time works, but swapping an entire lineup — the
agent model, the utility model, their profiles, the embed
and rerank lanes that ride along with them — is tedious to do by hand, and
easy to leave half-applied. A stack is a single named entry that bundles
a whole set of slot assignments together with the profile and model metadata
those assignments reference, so the bundle is self-contained and portable to
another machine. Applying a stack replaces your running slot lineup with
exactly what the stack describes, in one guarded operation.

What’s in a stack

A stack is one [stack.<slug>] entry in /etc/hal0/stacks.toml, managed
exclusively through the /api/stacks router.

At the top level, a stack carries name, description, author, icon,
tags, a schema_version it was written against, and an hal0_version
provenance stamp. The bulk of a stack is its slot rows — one entry per
slot the stack manages, each shaped like a slot-edit-drawer’s worth of
fields:

Row field What it captures
slot The slot name (existing, or new — created on apply).
model / profile References resolved against the stack’s own embedded models/profiles maps.
device, provider Device and provider label. Device is slot-owned, same as an ordinary slot.
capabilities Embed/rerank/stt/tts/vision capability children carried by this row, each with its own device/provider/model/enabled.

A row carries no tuning. The context window, flags, thinking default, vision
and speculation all belong to the profile the row names; to run one model with
different tuning, name a different profile.

A stack also embeds a transport-safe copy of every profile and model it
references, so importing it onto a fresh host doesn’t depend on that host
already having the right profile or registry entry.

Seed stacks

hal0 ships three built-in stacks tuned to the reference Strix Halo roster,
each pairing an agent and a utility slot and carrying dedicated embed and
rerank rows so memory recall works out of the box:

  • saber — a MoE agent model on the agent profile, paired with a fast utility model.
  • forge — a coding-focused agent/utility pair, both MTP-capable builds.
  • pi — a lightweight-host-oriented pairing.

Like seed profiles, the three seeds are immutable — applying a change to
one is rejected; clone it under a new name to customize it.

Applying a stack: plan, then converge

Applying a stack runs in two phases, and neither one can leave your platform
in a state a manual edit couldn’t also produce:

  1. Plan and commit. For every row that names a model against a slot that
    already exists, hal0 computes the resulting slot config and routes it
    through the exact same guarded pipeline every other slot writer uses —
    the tuning-key partition, NPU exclusivity, default-uniqueness. A row that
    would fail that pipeline is rejected and reported, but doesn’t abort the
    rest of the apply — one bad row never blocks the others. The result commits
    atomically.

  2. Converge. With the config committed, hal0 walks the live slot set and
    brings it in line: each primary slot is loaded (if offline), swapped (if
    running the wrong model), or left alone (if already correct) — a slot
    that’s mid-lifecycle (pulling, starting, warming, unloading) is never
    interrupted. Each enabled capability child (embed, rerank, stt, tts,
    vision) is applied the same way. Finally, every dispatchable slot the
    stack doesn’t mention gets unloaded.

That last step is what makes applying a stack a declarative replace, not
a merge: whatever isn’t in the stack goes dark. Any per-slot or
per-capability failure during convergence is recorded rather than raised, so
a single runtime hiccup never unwinds the config you already committed.

Drift: is the running config still what you applied?

hal0 tracks which stack is active and fingerprints the slot configuration at
the moment it was applied. Comparing that fingerprint against the live
config on every read gives one of four states:

Status Meaning
none No stack has ever been applied.
clean The live config still matches what was applied, and convergence reported no errors.
degraded The config still matches, but convergence hit a per-slot runtime error — the plan is honest, the runtime didn’t fully come up.
modified A slot was hand-edited since the stack was applied (or the active stack itself was deleted out from under the pointer).

Drift is a config-level signal, not a liveness probe — hand-editing a stack’s
slot afterward, even correctly, is enough to flip an active stack from
clean to modified.

Portable: export, import, snapshot

A stack can be exported to a self-contained, checksummed .hal0stack.json
envelope — no secrets, no host paths — and imported on another machine.
Import dry-runs first: it checks the envelope’s schema_version isn’t newer
than what the running hal0 understands, classifies every referenced model as
present / pullable / unresolvable against the local registry, and
reconciles embedded profiles against the local catalog — a local profile of
the same name always wins
, so importing a stack never silently overwrites
a profile you’ve already tuned.

You can also go the other direction: snapshot builds a brand-new stack
from whatever is actually running right now (live slot TOML plus capability
selections), so a lineup you assembled by hand one slot at a time can be
captured, named, and shared without retyping it as a stack from scratch.

The dashboard’s Stacks view

The dashboard’s Stacks view has two parts: a full-width Hero card for
the currently active stack — one row per slot with its model, device, and
the clean/modified drift badge, plus Edit, Export, and Re-apply actions —
and a Library grid of every other stack, with Load, Clone, Export, Edit,
and Delete affordances (Edit and Delete are hidden for the three immutable
seeds). Editing or creating a stack opens a row-by-row drawer: slot name
(with a “new slot” chip when it doesn’t exist yet), model, device, and a
profile picker.

Where to go next

  • Slots: The lifecycle and the slot / model / profile ownership split a stack converges against.
  • Capabilities & profiles: What a stack row’s profile and capability children configure.
  • Architecture: Where stacks sit relative to the dispatcher and slot manager.