Muse Code provider¶
This page describes how SASE integrates Muse Code for agent invocations.
Muse Code Integration¶
The MuseProvider invokes Meta's Muse Code CLI (muse).
Selection¶
Muse is never autodetected. It publishes llm_autodetect_cli_name but deliberately
no llm_autodetect_priority, so it never appears in autodetect candidates: muse is a
generic executable name, and SASE's autodetect only checks whether a binary of that name
is on PATH. Reach Muse with llm_provider.provider: muse, %model:muse/<model>, or
by pointing SASE_MUSE_PATH at the binary. Separately, whichever shipped size aliases
currently target Muse can select it whenever a muse executable is available; for Muse
that path lands on a Contributor model, which trains on its inputs and outputs (see
Model Mapping and the generated
shipped size-alias defaults).
provider_cli_available() still uses the CLI name, so sase doctor and the
sase agent-cli inventory see Muse normally.
Muse's provider short name is mus, which enables foo.mus agent naming.
Command Construction¶
MUSE_NO_AUTO_UPDATE=1 muse exec --json --workspace <cwd> --model <model> [--reasoning-effort <level>] \
--trust-workspace --disable-approval --disable-sandbox \
--user-input-auto-resolve --no-foreign-personal-context \
[--enable-shell-tool] --session-id <uuid> --prompt-file <tempfile> [extra_args...]
Decisions inside that command:
--prompt-file, not stdin and not a positional argument.muse execreserves stdin for--api-key-stdin, and SASE prompts routinely exceed comfortable argv limits. The prompt is written to a0o600file under SASE's managed temp root and removed as soon as the cycle ends.MUSE_NO_AUTO_UPDATE=1. The Muse launcher otherwise checks for and swaps in a new binary hourly; a multi-hour agent run must not have its binary replaced mid-flight. Update Muse throughsase agent-cli update museinstead.--session-idis generated by SASE, not left to Muse, because it is the handle that locates the session log SASE reads token usage from.- Sandbox off by default. Under Muse's sandbox,
.git,.muse, and.agentsare read-only inside the workspace root, which breaks any in-runsase stitch createan agent performs through thesase_git_commitskill. Disabling it matches what SASE already does for Codex and OpenCode. Approvals must go regardless — a headless run cannot answer them. - No
-w/--worktreeand no--subagent-worktree-isolation. SASE's workspace is the workspace, and subagent isolation is a documented no-op. --enable-shell-tool. SASE switches Muse to itsshelltool, which runs every command synchronously (see Single-turn normalization). SASE skips the flag when the resolved extra-args string already carries it: a duplicate boolean flag is amuse execusage error (exit 2). Human interactive sessions (llm_interactive_cli) keep Muse's defaults.
Set SASE_MUSE_SANDBOX=on for a hardened opt-in: SASE keeps Muse's sandbox and passes
--sandbox-network enabled instead of --disable-sandbox. This is containment SASE has
with no other provider and is genuinely useful for read-only research agents, but
in-run commits fail under it because the sandbox makes .git read-only.
Single-turn normalization¶
Muse runs every command synchronously inside its turn, and anything that can outlast Muse's synchronous ceiling goes to a SASE monitor, chosen before the command starts.
- Synchronous
shelltool. SASE launchesmuse execwith--enable-shell-tool: Muse runs every command synchronously in itsshelltool, which has a hard 10-minute kill and no post-turn background wake. - The
shelltool's 10-minute kill discards all output. A command still running at 600 seconds is killed and returns onlytool timed out, so final verification prefers prepared monitor completion and long commands start under/sase_monitor(see the directive). - Synchronous-ceiling export. Around every provider invocation SASE sets
SASE_PROVIDER_SYNC_CEILING_SECONDS=600, so in-harness tooling can read the kill ceiling instead of guessing it. The variable is scrubbed at every agent, monitor, and proc boundary, so a child never inherits its starter's ceiling. Beside it SASE exportsSASE_PROVIDER_SYNC_SOFT_CEILING_SECONDSfromtool_runs.soft_ceiling(unset when none is configured); it kills nothing and names the most time an agent should block on onesase tool runbefore escalating to a monitor. - Single-turn directive. Every Muse prompt carries a short prefix stating the
ceiling and the up-front routing rules: final verification prefers prepared monitor
completion (
/sase_final), commands that can take longer than 10 minutes go to/sase_monitorwith--nextbefore they start, and everything else runs inline, with commands of uncertain length other thansase tool runwrapped astimeout 540 <cmd> > <log> 2>&1; ...so a slow run still leaves evidence.sase tool runis the exception: it returns before the ceiling on its own and is never wrapped intimeout; when it escalates, the agent runs the printedsase monitor start -J ...join next. - Why managed
bashis not used. The managed tool backgrounds long commands and can wake the model after its turn ends. That wake is invisible to SASE: it never appears in the--jsonstream SASE reads, and it dies with the provider process. - Stranded-wait guard. After a clean exit whose reply still ends by claiming to wait
("I'll wait", "still running", and similar), SASE re-invokes Muse with the accumulated
reply plus a nudge to finish in the foreground or hand the long command to
/sase_monitor, up toSASE_MUSE_MAX_WAIT_CONTINUATIONScontinuations (default2). When the budget is exhausted the run fails withLLMInvocationErrorinstead of recording the waiting reply as a successful answer. Each firing is logged towait_guard_log.jsonlin the artifacts directory. The wait-signal pattern lives in the sharedsrc/sase/llm_provider/_wait_signals.pymodule, also used by Claude's wait guard.
Model Mapping¶
| Tier | Model |
|---|---|
large |
muse-wonder-1.0 |
small |
muse-spark-1.3 |
| Model | Context | In / Cached / Out (per 1M) | Notes |
|---|---|---|---|
muse-wonder-1.0 |
1M | $4.00 / $0.20 / $20.00 | Meta's frontier model (preview) for coding and long-running agentic work; Muse Code's catalog default. Every Muse Code plan and the Meta Model API. |
muse-wonder-1.0-contributor |
1M | no published price | Same capabilities as Wonder. Meta uses its inputs and outputs to train and improve Meta's AI models. |
muse-wonder-full-1.0 |
1M | subscription only | Larger Wonder variant. Muse Code Power and Maximum plans only; not served through the Meta Model API. |
muse-wonder-full-1.0-contributor |
1M | subscription only | Same capabilities as Wonder Full. Meta uses its inputs and outputs to train and improve Meta's AI models. |
muse-spark-1.3 |
1M | $1.25 / $0.15 / $4.25 | Lower-latency coding-optimized model for agentic workflows. |
muse-spark-1.3-contributor |
1M | $0.10 / $0.002 / $0.20 | Same capabilities as 1.3. Meta uses its inputs and outputs to train and improve Meta's AI models. Rate limited; select countries only. |
muse-spark-1.2 |
1M | $1.25 / $0.15 / $4.25 | Supported prior coding-optimized model. |
muse-spark-1.2-contributor |
1M | $0.10 / $0.002 / $0.20 | Same capabilities as 1.2. Meta uses its inputs and outputs to train and improve Meta's AI models. Rate limited; select countries only. |
muse-spark-1.1 |
1M | $1.25 / $0.15 / $4.25 | Agentic and multimodal (text, images, video, documents). |
Neither tier maps to a Contributor model on purpose (see the generated tier table
above). A tier mapping is SASE's own default choice of model, and mapping it to the
Contributor model would silently ship a user's proprietary source into Meta's training
corpus. Under large_tier = wonder, large follows Muse Code's Wonder default while
small stays on Spark 1.3.
The shipped size aliases are a separate route, and they can reach the Contributor
model. Whichever shipped size aliases currently include a Contributor member (see the
generated Implicit role aliases) route there
automatically whenever a muse executable is available — with no %model directive and
no config change — and Meta then trains on that agent's prompt, repository contents, and
tool output. To opt out, override the pool with
llm_provider.model_aliases.builtin.<size> using a target that omits the Muse member.
sase doctor -C llm.model_advisory and the model advisory
surfaces make the trade visible.
The Contributor models also remain reachable by name: each is a known model name, has a
short alias, and %model:muse/<model> works for either. A Contributor model absent from
the generated shipped-alias table is only ever reached by typing its name.
Muse Reasoning Effort¶
Muse accepts all seven canonical levels, including max. Meta documents max reasoning
for the Wonder models and Spark 1.3, including their Contributor variants, but not for
Spark 1.2. Other combinations are left to Muse Code to validate. Muse's own internal
default is high, so a run with no resolved effort shows blank in SASE while Muse
actually used high; the recorded model identity (below) closes the equivalent gap for
the model.
The Event Stream¶
muse exec --json writes pure JSONL to stdout; human diagnostics go to stderr. Every
line is an envelope carrying schema_version, payload_type, payload_schema_version,
and payload. SASE's parser rules, in priority order:
run.terminal.completed→payload.textis the authoritative reply.payload.terminalis the outcome andpayload.reasonthe detail; SASE parses those fields and never pattern-matches reply text.run.output.deltaupdates the live reply as fragments arrive. It is markedephemeraland carries incremental fragments, split mid-word and mid-inline-code. When a later terminal event carries text, those fragments are the pieces of that text. SASE coalesces the deltas of one run stream (keyed bycommand_id, thenrun_stream.id, otherwise one unkeyed stream) into a single timestampedlive_reply.mdchunk, so the panel shows one divider and intact prose per run. Each delta is appended to that file and flushed as it arrives. When stdout is Rich's LiveFileProxy, SASE does not flush the proxy after every fragment, so a streamed reply is not split across terminal lines mid-word. The open chunk is flushed to the terminal when that chunk closes: the next run stream, a matching terminal event, or the end of the subprocess read. Any other stdout still flushes each delta. A later run stream is written intolive_reply.mdafter a blank line. Deltas are not added on top ofpayload.text. When a terminal event carries text, that text is the returned reply. When no terminal event arrives, or a terminal event arrives with no text, the concatenated deltas are the reply. Only a missing terminal event is recorded as a schema diagnostic.- A failed, rejected, or cancelled task is not a failed run. Muse emits
task.lifecycle.rejected(reason: "skip_if_running") andtask.lifecycle.cancelled(reason: "main run completed") on runs that exit0. Success is gated onrun.terminal.*plus the exit code; task-level failures are recorded as diagnostics only. - Unknown payload types and higher schema versions do not raise. Parse failures surface the observed versions as a stdout-decode diagnostic rather than returning an empty success, and repeated schema diagnostics are capped.
- Exit code 2 is a
muse execusage error, not a run failure, and the raisedCalledProcessErrordiagnostics say so, so a bad flag does not read as a model failure.
Every flag and payload-type string lives in one module-level constant block in
_subprocess_muse.py, so a beta rename is a one-line fix.
Muse Tool-Call Capture¶
SASE builds tool-call records purely from the stdout stream; it does not wire Muse's
hook system and does not read Muse state off disk for this. When SASE_ARTIFACTS_DIR is
present, normalized records are appended to $SASE_ARTIFACTS_DIR/tool_calls.jsonl with
runtime: "muse" and source: "stream" for sase's TUI
Agents Tab LLM Calls Panel.
Fixture coverage is keyed to Muse release 0.1.0-R708.1.
| Event | Carries | Use |
|---|---|---|
task.lifecycle.proposed |
task_kind: "tool.<name>", task_id |
Opens a pending call — only for task_kind values under tool. |
task.lifecycle.scheduled / side_effect_intent |
idempotency_key: "tool:<call_id>", operation, policy_decision |
Binds task_id → call_id |
task.lifecycle.output |
event.chunk |
Streamed tool output |
tool.result |
call_id, correlation_facts.{tool_name,outcome}, optional edit_facts |
Closes the call with its outcome and result |
Tool arguments are never in the stream. SASE derives each record's target honestly
and in this order: edit_facts.path when present; for bash (or shell, when its body
carries them), the command and description fields of the result JSON; otherwise a
truncated preview of the result text. It does not invent arguments Muse did not emit.
Non-tool tasks (model.meta.response, reminder.agent.plugin:*) never become tool
records, and calls still pending at stream end are finalized like every other
provider's.
shell tool. Under muse exec --enable-shell-tool (fixture
tests/llm_provider/fixtures/muse_exec_shell_tool_R3401.1.jsonl, keyed to Muse release
1.3.0-R3401.1), tool tasks arrive as tool.shell with plain-text results that carry
no command / description fields, so their target is honestly the result preview —
SASE never invents the command. They display as Bash. A timed-out shell call
(correlation_facts.outcome of timeout / timed_out, text tool timed out) is
recorded as a failure, never a success, with the timeout text kept visible in the
response summary.
Token Usage and Model Identity¶
Muse's stdout stream carries no token counts at all; the numbers live in the on-disk
session log. Because SASE passes --session-id, that location is deterministic:
$XDG_DATA_HOME/muse/sessions/YYYY/MM/DD/<session-id>/session.jsonl
(XDG_DATA_HOME defaults to ~/.local/share; the date components are globbed rather
than computed from today's date so a run spanning midnight still resolves.) After the
subprocess exits, SASE sums usage across runtime.session events whose
payload.event.kind is model_completed, mapping input_tokens, output_tokens,
cache_read_tokens (falling back to the older cached_tokens), and
cache_write_tokens onto SASE's counters. goal_usage_attribution events repeat the
same numbers for the same call and are deliberately ignored — counting both would double
every run's totals. A missing or unreadable session log is not an error: it degrades to
zeroed usage plus a diagnostic.
SASE does not shell out to muse export for this. It costs a subprocess, --redacted
strips the call_ids, and unredacted output contains verbatim encrypted reasoning SASE
has no reason to retain.
run.model.configured carries the model Muse actually configured. SASE records its
model_id, provider_id, and the session id into run_metadata.json, which closes the
observability gap where a run with no explicitly resolved model shows blank in SASE
while Muse used its own default.
Interrupts and Retries¶
Muse has no headless resume (muse resume is interactive-only), so interrupt handling
reuses the accumulated-context restart that Qwen, OpenCode, and Codex use: SASE
reconstructs a continuation prompt and relaunches. The session log is kept for manual
recovery. Muse retries its model stream internally. When that budget is exhausted on a
transient model-service failure, it exits 1. Muse's provider-supplied retry defaults
then re-run the agent in a fresh Muse session, with the workspace preserved and the
resume nudge prepended.
Skills and Instruction File¶
SASE deploys Muse skills under ~/.config/muse/skills/<skill>/SKILL.md, rendered with
provider_name: "Muse Code". Without that deploy path Muse picks up SASE's
Claude-rendered skill copies from ~/.claude/skills/ and reads them as if it were
Claude Code. Muse reads AGENTS.md natively, so there is no MUSE.md provider shim.
Environment Variables¶
| Variable | Description |
|---|---|
SASE_LLM_LARGE_ARGS |
Extra CLI args for large tier (generic, preferred) |
SASE_LLM_SMALL_ARGS |
Extra CLI args for small tier (generic, preferred) |
SASE_MUSE_PATH |
Path to the Muse Code CLI binary (default: muse on PATH) |
SASE_MUSE_LARGE_ARGS |
Extra CLI args for large tier (Muse-specific fallback) |
SASE_MUSE_SMALL_ARGS |
Extra CLI args for small tier (Muse-specific fallback) |
SASE_MUSE_SANDBOX |
Set to on to keep Muse's sandbox with --sandbox-network enabled |
SASE_MUSE_MAX_WAIT_CONTINUATIONS |
Stranded-wait guard continuation cap (default: 2) |
The generic SASE_LLM_*_ARGS variables take precedence over SASE_MUSE_*_ARGS.
Timer Display¶
While waiting for a response, a provider_timer("Waiting for Muse Code") spinner is
shown (unless suppress_output is True).
Related¶
- Providers and models — the provider integration overview.
- Provider usage — subscription usage reporting.
_[project]: A sase project is a named unit of work registered with SASE. _[workspace]: A sase workspace is a numbered clone of a project's primary repo, managed by the workspace store and tracked in that project's registry.json. _[agent]: A sase agent is an agent session or a single agent that does not belong to a session. _[prompt]: A prompt is the text SASE hands to a provider to start an agent turn. _[macro]: A macro is a reusable prompt triggered with #name in agent prompts. _[Patch]: A Patch is SASE's local unit of change. _[bead]: A bead is one tracked work item in SASE's git-native issue tracker. _[plan]: A plan is a persisted, approved artifact recording the intent behind agent work. _[TUI]: The TUI is SASE's terminal interface for running agents and reviewing results. _[epic]: An epic is an executable multi-phase plan run with sase bead work. _[gate]: A sase gate is a durable, command-backed request for one user decision, created when work must pause for the user. _[monitor]: A sase monitor is a monitor turn that runs one long command under a detached supervisor, so the command outlives the agent that started it. *[stitch]: A stitch is the lightweight ordered change record inside a Patch's STITCHES: section.