docs
Composability
Most agent harnesses hard-code the decisions that make an agent an agent: which tools the model sees, when to compact, when to ask for approval, how a turn loops. In dex, those decisions are slots on a harness graph — each with a Rust default — and you can select, wrap, or replace any of them at runtime, from config or Lua, without recompiling dex.
The kernel stays fixed: workspace confinement, credential handling, sessions, cancellation. The harness — the components that make per-turn decisions — is the composable part.
The slots
Every turn resolves one immutable harness snapshot; the turn loop asks the snapshot and never reads global mutable state, so a turn can never see a half-applied composition.
| Slot | Decides | Default |
|---|---|---|
catalog | Which tool schemas the model sees (native + MCP + extensions) | composed catalog |
trigger | Whether a turn needs compaction | config budget |
summarizer | The compaction summary | deterministic checkpoint |
overflow | Context-overflow detection | threshold detector |
conflict | Whether a tool batch has conflicting calls | conflict detector |
scorer | Which stale tool outputs to prune | heuristic scorer |
approval | Tool approval policy | built-in permission policy |
executor | How tool calls execute | workspace-confined dispatcher |
transcript | Where transcript lines go | session store |
usage | Token/cost accounting | token ledger |
events | Event sink | transcript events |
recovery | Error recovery policy | built-in recovery |
Plus one Lua-only slot, agent_loop, which owns the whole
turn loop.
Select, then inspect
The floor is config — pick a named implementation per slot:
harness:
slots:
catalog: native-only # native tools only — no MCP/extension tail
trigger: never # never auto-compact
Whatever you select, dex runtime graph prints every slot's
resolved id and origin — the same resolver the turn loop calls, so the
graph cannot drift from what a turn actually runs:
dex runtime graph
slots:
agent_loop run_turn rust, builtin
catalog native-only config harness.slots
trigger never profile 'lean'
summarizer default built-in default
... A bad selection never fails a turn: unknown slots or ids warn once and keep the default.
Implement slots in Lua
A harness-capability extension can implement slots as Lua
components. Here a router decides which model each turn serves:
# manifest.yaml
dex: ">=0.15"
capabilities: [harness]
components:
model_selector: router.lua
# router.lua — pick the model one turn serves
return function(dex)
dex.use("model_selector", {
id = "cost.router",
interface = "model_selector.v1",
run = function(input)
return { model = "opencode/grok-code" }
end,
})
end dex.use selects, dex.wrap adds middleware
around an existing implementation, dex.fallback installs a
backup. Interfaces are versioned (model_selector.v1) and
checked at load — a component written against a different envelope fails
loudly instead of misreading payloads.
The ladder goes to the top
Depth is a spectrum, and the deepest rung replaces the turn loop itself:
- Config —
harness.slots:selects named implementations. - Profiles — a named slot map
(
harness_profiles:) applied transactionally; a Lua extension can switch them at runtime withdex.activate_profile(name). A profile never half-applies. - Components —
dex.use/dex.wrap/dex.fallbackper slot. - The loop —
dex.replace("agent_loop", …):
dex.replace("agent_loop", { id = "my.loop", interface = "agent_loop.v1",
run = function(ctx)
while true do
local r = ctx.model.call()
if r.tool_calls and #r.tool_calls > 0 then
ctx.tools.execute()
else
local s = ctx.finish(r.content or "")
if not s.steered then return r.content or "" end
end
end
end })
Your Lua owns iteration; Rust keeps every invariant — persistence,
streaming, cancellation, budgets, the history ledger. The step surface
(ctx.model.call(), ctx.tools.execute(),
ctx.finish()) is the whole API, and the default loop is the
same six lines.
What you can't break
All host operations pass through Rust primitives; Lua policy sits above the runtime policy, and the kernel below both is authoritative:
Lua policy → Runtime policy → Kernel enforcement - Capabilities are enforced at registration — a plain
toolsextension can never see or decide an approval. - Hooks may deny or escalate, never clear a host denial.
- A broken component fails open to the Rust default; it never crashes the process.
Everything else you'd expect from a composable harness is here too —
skills,
MCP tools,
extensions, and a pipe-friendly
one-shot CLI all enter through the same schema and transcript. But that's
the surface: the harness graph underneath is what makes "change anything"
literal. The full reference lives in
extensions and the repository's
docs/runtime.md.