Skip to content

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 exec reserves stdin for --api-key-stdin, and SASE prompts routinely exceed comfortable argv limits. The prompt is written to a 0o600 file 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 through sase agent-cli update muse instead.
  • --session-id is 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 .agents are read-only inside the workspace root, which breaks any in-run sase stitch create an agent performs through the sase_git_commit skill. Disabling it matches what SASE already does for Codex and OpenCode. Approvals must go regardless — a headless run cannot answer them.
  • No -w/--worktree and 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 its shell tool, 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 a muse exec usage 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 shell tool. SASE launches muse exec with --enable-shell-tool: Muse runs every command synchronously in its shell tool, which has a hard 10-minute kill and no post-turn background wake.
  • The shell tool's 10-minute kill discards all output. A command still running at 600 seconds is killed and returns only tool 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 exports SASE_PROVIDER_SYNC_SOFT_CEILING_SECONDS from tool_runs.soft_ceiling (unset when none is configured); it kills nothing and names the most time an agent should block on one sase tool run before 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_monitor with --next before they start, and everything else runs inline, with commands of uncertain length other than sase tool run wrapped as timeout 540 <cmd> > <log> 2>&1; ... so a slow run still leaves evidence. sase tool run is the exception: it returns before the ceiling on its own and is never wrapped in timeout; when it escalates, the agent runs the printed sase monitor start -J ... join next.
  • Why managed bash is 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 --json stream 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 to SASE_MUSE_MAX_WAIT_CONTINUATIONS continuations (default 2). When the budget is exhausted the run fails with LLMInvocationError instead of recording the waiting reply as a successful answer. Each firing is logged to wait_guard_log.jsonl in the artifacts directory. The wait-signal pattern lives in the shared src/sase/llm_provider/_wait_signals.py module, 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:

  1. run.terminal.completed → payload.text is the authoritative reply. payload.terminal is the outcome and payload.reason the detail; SASE parses those fields and never pattern-matches reply text.
  2. run.output.delta updates the live reply as fragments arrive. It is marked ephemeral and 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 by command_id, then run_stream.id, otherwise one unkeyed stream) into a single timestamped live_reply.md chunk, 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 Live FileProxy, 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 into live_reply.md after a blank line. Deltas are not added on top of payload.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.
  3. A failed, rejected, or cancelled task is not a failed run. Muse emits task.lifecycle.rejected (reason: "skip_if_running") and task.lifecycle.cancelled (reason: "main run completed") on runs that exit 0. Success is gated on run.terminal.* plus the exit code; task-level failures are recorded as diagnostics only.
  4. 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.
  5. Exit code 2 is a muse exec usage error, not a run failure, and the raised CalledProcessError diagnostics 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).

_[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.