Running a Muse session
Class: drive-only. Nothing here rebuilds aether's engine: you start one
long-lived Bloomery engine under the hub, bind the Muse bundle on it once, and
then drive any number of sessions with five cargo xtask muse verbs. Each verb
prints a few key=value lines and keeps the long waits and the file bytes to
itself, so a caller sequences them and reads only those lines.
A session is the muse.session reactor loop in crates/aether-bloomery-muse
(ADR-0234): muse.session.open starts one on a tree, the loop runs turns and
the tool calls they ask for, and every rest is recorded as a muse.session
whose head is named by the session key. The verbs speak the engine's own
journal and driver mail over its RPC port: aether.bloomery.journal.stage,
publish, read_events, watch_head, and read_artifacts to the unit's
journal owner, and aether.bloomery.driver.call to the driver.
1. Start a Bloomery engine under the hub
The engine needs egress to the model vendor and a credential for it. Put the
key in a secret file and bind it on aether.http exactly as
Supplying secrets describes (its sections 6 and 8 cover
a hub-spawned engine and the Bloomery engine). Upload the aether-bloomery
binary with upload_binary, then fork it with spawn_substrate, passing the
engine's flags through args:
spawn_substrate(selector=<the uploaded aether-bloomery>,
args=["--bloomery-units", "primary=<journal directory>",
"--secrets-dir", "<directory holding only this engine's secrets>",
"--http-allowlist", "api.muse.example",
"--http-secrets", "api.muse.example/bearer=muse"])
Keep the rpc_port the reply reports (list_engines shows it again later):
every verb below takes it as --rpc-port, and --unit is the unit key the
--bloomery-units entry names (primary here). Each verb dials
127.0.0.1:<rpc-port>, so run them on the host the engine runs on.
2. Bind the bundle, once
Build the bundle with cargo xtask build-wasm, then:
cargo xtask muse bind --rpc-port <port> --unit primary --bundle <path to aether_bloomery_muse.wasm>
bound bundle=<digest> set=<digest>
bind stages the bundle and, in one fenced publish, moves the muse head to
it and the reactor-set root (core.reactors) to a set holding muse beside
every member the bound set already holds. A rebind of the same bundle prints
unchanged and publishes nothing. Rebind after rebuilding the bundle.
After the publish, bind waits for the driver to record the reactor's
activation. If the driver rejected it, bind exits non-zero naming the
rejection's reason, its journal entry, and the remedy: stop the engine, move
the directory --bloomery-units <unit>=<dir> names aside, restart, and bind
again. A journal recorded under an older stored encoding is expected to need a
fresh directory this way. A rebind that publishes nothing fails the same way
when the muse head is owed from an earlier rejection.
3. Open a session
open first reads the journal and refuses, before staging anything, when the
muse head is not live: it names the same recorded reason and remedy, or says
to run muse bind when the head was never activated.
cargo xtask muse open --rpc-port <port> --unit primary \
--commit <revision> --brief brief.md --instructions CLAUDE.md --seeds seeds.txt \
--endpoint https://api.muse.example/v1/responses --model <model> \
--effort medium --max-output-tokens 16000 --max-turns 40 --input-limit 184220
tree=<digest>
session=<key>
after=<seq>
--commitimports the files the commit tracks, exactly ascargo xtask import-commitdoes (see Workspace), in the repository around the working directory.--tree <digest>names a tree already in the journal instead.--briefis a file holding the first user message.--instructionsis required: a file holding the session instructions, sent as the leading developer message ahead of the brief with a short preface saying the session works only through the offered tools, makes every independent call in the same turn, runstree-diffbefore endingDone, and ends its run only by callingmuse-end; with proofs offered, it also tells the model to runproof-clippyandproof-testuntil each passes before endingDone, and with proofs required, that endingDoneruns them.--seedsis optional: a file naming one tree path per line, blank lines skipped. Each is read withtree.readbefore the first turn, so the model starts with those files in view.- The settings have no defaults.
--effortislow,medium, orhigh.--input-limitis the most input tokens a turn may be billed for before the session restscontext-full; a value of 0 is refused. - Every bound tool is offered:
tree.list,tree.read,tree.grep,tree.diff,tree.edit,tree.write,tree.remove,tree.move,muse.echo, andmuse.end. The model reads a read's text and an edit's summary exactly as stored, so the text after a line's number and tab is whattree.editmatches. A reply without a tool call does not end the session: the loop nudges the model back for another turn. The first turn sends the instructions as the developer message ahead of the brief.tree.diffshows the session's tree against the tree it opened on. --environment <digest>and--vendor <digest>, given together, also offerproof.clippyandproof.testfrom theworkspace-programsbundle, bound to that environment, vendor tree, and the--test-envvariables (see The proofs). Each formats the session's tree withcargo fmtand returns the formatted tree, which becomes the session's tree as an edit's does:proof.clippychecks it with workspace-widecargo clippy, andproof.testruns its workspace tests with the session's test env, or a scoped subset while iterating (a crate's unit tests by filter, an integration target by--testname, both combined); theDonegate still runs the whole workspace. The same flags also offervendor.list,vendor.read, andvendor.grep, which taketree.list's,tree.read's, andtree.grep's arguments and read the--vendortree, one directory per vendored crate, so the model reads a dependency's API before it calls it. The engine must run the workspace (a Docker daemon at--workspace-endpoint;--workspace-warm-layersbuilds over warm layers), have theaether_bloomery_workspace_programsbundle bound at the headworkspace-programs, and hold both digests: the environment the head(aether.workspace.environment, <platform>)names, and theVendored.treeof avendor.cargorun over a source with the session tree'sCargo.lock. On an engine the hub did not spawn,muse bind-programs --bundle <wasm>binds the bundle head,muse bootstrap --base <ref> --toolchain <ref>prints the environment digest, andmuse vendor (--commit <rev> | --tree <digest>) --environment <digest>printsvendor=<digest>, or fails with cargo's stderr. A proof that runs out of time or memory is retried twice, then answered with what happened; any other proof fault fails the session.--test-env KEY=VALUE, repeatable, needs--environment/--vendor: a variable the test proof hands cargo. This repository's lane passesAETHER_ALLOW_WASM_SKIP=1, so a SubstrateHarness scenario whose component wasm was not built skips instead of failing;AETHER_STORE_PATH=:memory:, so no store-backed test opens a real journal; andAETHER_HARNESS_FLEET_BIN_DIR=/work/target/debug, so the FleetHarness suites that fork a chassis resolve the bins the test build already produced.--require <program>, repeatable, needs--environment/--vendor: a proof tool (proof.clippyorproof.test) aDoneend must pass on the session's tree, over the whole workspace. It is explicit: offering the proofs requires none of them. When the model endsDone, the loop runs each required proof it has not already passed on the current tree (the model's own passing run on that tree counts), and the session restscompletedwith the tree the proofs left, formatted bycargo fmt. A failed proof answers themuse-endcall with the proof's summary and diagnostics instead, and the session goes on; that failed gate spends the turn, so a session that cannot get green still rests at--max-turns. A required proof that runs out of time or memory three times answers the end call the same way.BlockedandAskedends are not gated, and a continue keeps the gate.--share-cache-key <text>: the prompt cache key this session shares with every other session opened with it, drawn fresh when omitted.
The call key is derived from the open's input digest, so running the same
open again (after a lost reply, say) prints the same session instead of
opening a second one. A recorded fault is asked again under the next attempt's
key, so rerunning after a fault outside the input makes a fresh call. The
digest covers the instructions with the tree, brief, seeds, settings, and
required proofs. open returns once the session's open is recorded and does
not wait for the session's turns. Keep session= and after= for the next
step.
4. Wait for it to rest
cargo xtask muse wait --rpc-port <port> --unit primary --session <key> --after <seq>
turn=21931 input=18422 cached=17920 output=311 reasoning=96
unreported turn=21940
rested completed turns=7
from=<tree digest> to=<tree digest>
usage input=184220 cached=151040 output=9311 reasoning=4096
<the final assistant message>
wait reads the journal from --after and blocks on its head until the
session's head moves to its next record. It counts only the session's own
entries: an entry belongs to the session when its cause chain reaches the
session's open run, or a continue run that names the session, so twenty
sessions on one engine are each followed by their own wait.
restediscompleted,blocked,asked,declined,incomplete,turn-limit,context-full, orfailed: <why>.completedmeans the run endedDonewith a summary,blockedthat it endedBlockedwith what stopped it, andaskedthat it endedAskedwith the one question it cannot go on without. The final message follows for all three end reasons: the summary, reason, or question. Themuse-endcall takesending, one ofDone,Blocked, orAsked, andtext, a plain string. A journal that holds end calls from before this change does not replay them.turnscounts the session'smuse.turnruns read, andusagesums the token counts they reported.- Each
turn=line is one turn's own tokens, keyed by the journal seq of itsmuse.turnrun; a turn the vendor answered without usage printsunreported turn=<seq>. fromis the tree the activation started on (the open's tree, or the tree the continued record held) andtothe tree its record holds.- A fault or failed reaction in the session's chain is printed after the rest
and exits non-zero naming it. A second one, or a failed head move, means the
session cannot record its rest;
waitexits non-zero at once instead of blocking. A tool run that ran out of time or memory is not printed: the loop runs it again, up to twice, then answers the call with a text saying so, and the session goes on.
5. Export the changes
cargo xtask muse export --rpc-port <port> --unit primary --from <from> --to <to> --into <checkout>
M scripts/run.sh
A src/new.rs
D src/old.rs
export walks both trees through batched artifact reads, writes every added
or changed file with its execute bit and every symlink, and removes every
deleted entry, printing A|M|D <path> in path order. Run it on a clean
checkout of the commit the session opened on, and git diff there is the
patch; git add -A && git diff --cached includes the added files.
6. Continue
cargo xtask muse continue --rpc-port <port> --unit primary --session <key> \
--message next.md --max-turns 20
after=<seq>
continue finds the session's latest record (its head's last move), stages the
message, and calls muse.session.continue; pass the printed after= to the
next wait. An asked session resumes with its context once answered: pass the
answer as the message text. Without --message the conversation is resent as
it stands, which recovers a session that rested failed, on a vendor 504 for
instance, or incomplete with no output. --max-output-tokens optionally
replaces the session's output budget from that turn on.
The session keeps working on the tree its record holds, so edits made in the
checkout between rounds are not visible to it. Feed back compile errors or CI
failures as the message text. Each wait prints its round's from and to,
so exporting them in turn into the same checkout brings it along round by
round.