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 theagentprofile, 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:
-
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. -
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.
