magus v0.4.3 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
13 min read

ADR 0009: cost profiles for agent sessions

Decision accepted (in progress) on .

Each item below carries its own state: done is in the tree, planned is decided and queued, and not built was weighed and declined.

Context

A root agent session re-sends its whole context on every call, mostly as cache reads, so its cost grows with the square of its length. Measured on this repository's Claude Code transcripts from 2026-10-01 to 2026-10-13, over 26 root sessions and 569 subagents:

  • The root ran on Opus for 99 percent of its calls and carried 35 percent of all input tokens, 5.53 billion.
  • Nine of 26 roots reached about 960 thousand tokens of context. The median peak was 615 thousand, and the first call of a session started at 79 thousand.
  • Recursive search, whole-file reads, git diff, git log and non-gate magus run calls made up 63 percent of the root's tool-result characters, and 105 streaks of eight or more of those calls ran without a spawn.
  • 164 Opus spawns carried a prompt shaped like a scout, a read-only job or research.

The same person wants two policies, chosen by account and not by repository. On a personal subscription the bill is theirs and the root may carry more. On a shared account the cost is visible to a team. Billing is a fact about the account. How hard the guard pushes is a policy. They are two settings in two homes, and magus has to work outside this repository, for a person who only wanted a task runner.

Decision

One level, set by a call

Done. magus\guard.profile(level) picks the level the guard's compiled rules start from: off, light, standard or strict. A level sets the rules it names, and a rule named in magus\guard.builtins overrides the level's setting for that rule. It is declared once at the top level of the root magusfile. Unset is light.

When the magusfile is tracked, the stricter of the committed and the working-tree level applies, as it does for each builtins setting, so a loosening takes effect only once it is committed.

A person's agents.profile in magus.yaml can raise the level and never lower it, and of the tiers that set it the highest applies. The workspace sets a floor and the person who pays chooses to go above it.

Facts live in magus.yaml

Done. The agents section holds what only the person or the account owner knows:

agents:
  billing: metered # metered or quota; unset is unknown
  profile: strict # a person's level, raise only
  models: # adds or overrides rows of the model table
    claude-haiku-5-5: { window: 1000000, cache_read: 0.01, price_step: 100000, step_factor: 5 }
  • Billing is detected from the host before it is read from config: a hook environment with ANTHROPIC_API_KEY, CLAUDE_CODE_USE_BEDROCK or CLAUDE_CODE_USE_VERTEX is metered. An OAuth sign-in is ambiguous, because a usage-billed enterprise seat and a Max plan look alike, so it stays unknown unless configured.
  • A declared quota in an environment that shows metered billing is an error naming both, because the declared value is not what is happening.
  • A workspace's own magus.yaml wins over the user tier for billing, as layering always does, so a company repository can state metered for everyone who works in it.
  • The fields carry no command-line flag, and there is no environment variable.
  • Billing changes thresholds and wording and never which rules run.

Model facts come from three places, in order

Done. The host payload comes first, then agents.models, then the rows each harness spell the workspace wires publishes through its harness_models op; claude-code ships the Claude rows. A window the host states is never second-guessed. A model that matches no row gets no dollar wording and no window-relative threshold, and the absolute token thresholds still apply. No model name or price is compiled into magus, so a spell's rows are as current as the spell, and nothing is fetched at run time.

A root-context-budget tokens figure above every window in the table is an error, said as a refusal of the main thread's commands. The magusfile load cannot raise it, because the harness rows that make the table load after the magusfile, so the guard raises it where the table is known.

The root is the main thread

Done. The cost rules judge the main thread of a session, not an unleased session, which also covers a subagent without a lease and a person. A guard request carries the thread (main, sub or unknown), whether the host said so, and the context the host reported.

host main thread when context tokens from
Claude Code the call has no agent_id the last usage line of the main transcript
OpenCode the session has no parent the plugin sends the last message's tokens
Codex no lease, and the session recorded a spawn the transcript's last token_count event
Cursor no lease, and the session recorded a spawn preCompact; the last value stands until then

An unknown thread caps every verdict at advice. A wrong deny blocks real work, and a wrong advice costs one line.

The rules and their levels

Done. Seven rules join the compiled defaults, each disableable by name and each firing once per session per threshold:

rule light standard strict
root-context-budget advise once at half the window advise at 250k metered, 400k quota, 300k unknown advise at 150k, 250k or 200k; deny delegable commands from 300k, 450k or 350k
root-heavy-command off advise after 8 delegable calls since a spawn advise at 5, deny from 8
root-result-cap advise once above 40k characters 20k, once per tool class 10k, once per tool class
root-repeat off advise on the second identical read or command deny the third
spawn-names-model off advise on a spawn naming no model deny a spawn naming none, or one not at its job row's model tier
spawn-above-model-tier off advise on a spawn whose model tier ranks above its job row's the same; never deny
spawn-above-parent-tier deny a spawn whose model tier ranks above its spawner's the same the same

The delegable commands are recursive search, cat and sed -n of source, a whole-file host read, git diff and git log -p without --stat or --name-only, non-gate magus run and web fetches. The root may always spawn, run the magus job verbs, read a ref with magus query output, run the final gate, load a skill, read orientation files and edit.

The spawn rules read model tiers

Done. A job row records the model tier it was forked at: its name and rank in the workspace's model-tier declaration (magus\job.modelTiers, or the shipped magus/modeltiers module), the source that chose it, the reason and the declaration's line. The spawn rules resolve a spawn's model to a tier through the spawning harness's aliases in that declaration and compare ranks, never model names or prices.

  • spawn-names-model at strict refuses a spawn whose tier is not its row's. The refusal names the assignment whole, such as economy (rank 0) -> haiku, matrix: "two files" at hack/tiers.buzz:12. A harness the declaration maps no model for is refused naming the hosts entry to add.
  • spawn-above-model-tier advises on a spawn whose tier ranks above its row's, and never denies: work can outgrow its row, and the remedy is a re-fork that records why.
  • spawn-above-parent-tier refuses a spawn whose tier ranks above its spawner's: a child never outranks its parent. The spawner's model is the one its host reported, and an inferred one only advises. A model that is no exact alias, such as a transcript's full id, has an unknown tier; magus never infers one and advises naming why. The way up is a person forking the higher-tier row, and magus job fork holds a child row to its parent's tier the same way.
  • model-tier-context-cap advises a leased worker whose reported context passes its tier's maxContext bound, and set to deny refuses the worker's writes, as a passed deadline does. It advises at every level by default; the shipped declaration caps only economy.

A row forked with no declaration, or a spawn bound to no row, has nothing to compare, so the rules stay silent: a workspace with no tiers and no harness spells sees none of this. Magus never picks or changes a tier on its own. A rule reports or refuses; the row changes only when someone re-forks it or edits the declaration or the matrix.

A workspace that wants prompt-shape matching writes it as its own magus\guard.spawn rule, which already receives the prompt.

The stall rule

Done at level 3; planned for the levels above it. A worker that is blocked on something only the root can clear needs one clear exit, not a longer wait. The stall rule classifies a refusal the rules already make as fixable by the worker or not. When an unfixable one fires for a leased worker, the verdict ends with "exit the job with this as an unresolved risk", and one attention row is filed per lease and rule, so the root and the person see the block once.

  • light ships this level only. The levels above it, a repeated refusal and a run of failed calls, stay with standard and strict, and strict uses lower counts.
  • A row states facts: the model the job ran on and how often it was blocked. It never suggests a model. The root decides whether to re-fork the job with another.
  • Per-model ladders wait for two weeks of shadow verdicts. A model tier gets one only where it stalls at least twice as often per thousand calls as the others.
  • The rule never denies, and it adds no verb or flag.

Record what happened, so profiles are tuned from data

Done. Each agent call records its thread, context tokens, model, billing and result size. Each root call records what every level would have decided, whichever is active, so "how often would strict have denied" has an answer with no experiment. Reports fold into magus session show and its JSON, and metrics go through the existing OTEL path.

item State
magus\guard.profile, the levels, and the stricter-wins merge done
agents.billing, agents.profile, agents.models and host billing detection done
model rows from harness spells, under agents.models done
thread, context and billing on guard requests and trail records done
cost sections in magus session show done
the six cost rules and the level table done
the spawn rules by model tier rank, and model-tier-context-cap done
the stall rule at level 3 done
the stall levels above level 3 planned
shadow verdicts per level, and OTEL metrics done
per-model stall ladders not built

Principles checked

Each design principle below is held up against this decision.

  • Human-first is the AI integration: relied on. The level is a magusfile call and the facts are magus.yaml keys, with no command-line flag that feeds a model.
  • Friction that carries meaning: relied on. Each advisory fires once per session per threshold, because an advisory that fires on every call is a toll.
  • No silent defaults: relied on. An unknown level is MGS1045, a model row with no window fails at load, a declared quota under a metered environment is an error, and so is a budget tokens figure past every window in the table.
  • Agents propose, humans dispose: relied on. A stall files a row for the root and the person to read, and never picks the remedy.
  • Told, never guessed: departed from, in two places. Billing is read from the host's environment, and a Codex or Cursor session that records a spawn and has no lease is taken for a root. Both change advice only: wording, thresholds and an unknown thread's verdict capped at advice. Neither changes what a command does, and a declared value that contradicts a detected one is an error. The departure was Eli's decision on 2026-10-13, and agents.billing stays as the way to state the mode outright.
  • The outer harness stays outside: pending, because the principle sits on an unmerged branch (feat/proofread-library-improvements-87c508). The decision is meant to agree with it. The guard judges and informs, and no rule here trims, rewrites or proxies what the host's loop sends the model.

Alternatives

  • The level as a reserved key in magus\guard.builtins. It needed no new call and reused the stricter-wins merge, and the first design recommended it. The decision went the other way: a key that is not a rule name inside a map of rule names would need validation and describe rules to treat one key differently, and a call of its own keeps rule names and the level apart. Reopen if the level and the rules need to be declared together.
  • An environment variable for billing. One export per account would set it for every workspace in a shell. A stray export would flip every workspace silently, and the environment namespace is open. Host detection plus magus.yaml covers both accounts a person has. Reopen if a host appears that bills per token and shows nothing in its hook environment.
  • A default of off. light says one thing once per session, at half the window and at one 40 thousand character result, and only reaches a person already running an agent with magus hooks. Reopen if a measured share of light advisories draws a complaint.
  • Matching spawn prompts for economy. A pattern over 164 prompts is a guess, and a read-only design job looks the same as a search job. The job row's model tier is the field that says which a job is, so the spawn rules read it instead.
  • Ranking models by price. The first spawn-above-model-tier, then named spawn-principal-for-economy, called a model costly when its input price sat above the table's median. A price says nothing about which work a model suits, and the median moves whenever a row is added. Ranks in a declaration the workspace owns say it outright.
  • Fetching prices at run time. The Models API needs a key a subscription user lacks, and a hook already misses its 10 second budget under load. Reopen if harness spells' rows go stale faster than a spell release and agents.models can cover.
  • Stall behavior per model before data. The 14 days to 2026-10-13 showed 3.0 to 5.1 runs of three or more failed calls per thousand across Haiku, Sonnet, Opus and Fable, and about 30 percent of every tier's refusals repeated one already given. The Haiku sample is 33 agents. Reopen when shadow verdicts show a tier stalling at twice the rate of the rest.

The stall rule's declined designs, a model that watches workers, the root as the watcher, and a denial from the gauge among them, are rows in A record of refusals.

Consequences

  • A workspace with no agent hooks never meets any of this. A person with one agent and no subagents sees at most one advisory per session at light.
  • magus.yaml gains a section only an account owner can fill, and a work laptop's user tier can say metered for every repository on it.
  • The thread rules depend on what each host reports. Codex and Cursor cannot name the main thread, so their cost rules advise at most.
  • Reading the main transcript on a post-tool hook changes an old invariant, that magus never reads that file. The read is bounded to a tail and runs after the call, so a pre-call rule reads recorded state only.
  • A harness spell's model rows need an update when a model's price changes, or a person's agents.models row in the meantime.
  • Grading a spawn against its row's tier, or a worker's context against its tier's cap, opens the workspace to read the declaration, once per hook process and only for a row that records a tier; the cap is then kept for the session.

Open questions

  • Which prompt-cache time-to-live each host uses. It sets what an idle root costs while it waits on workers, and the numbers here assume the five minute write price.
  • Whether Claude Code hooks inherit ANTHROPIC_API_KEY and the Bedrock and Vertex variables in every launch mode, which billing detection relies on.
adrdecisionagentsguardcostconfigprofilesstall
Last updated (2df714e8)
Earlier changes on this page (1)

Full history ↗ · Blame source ↗

Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. Workspace in full.

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. Magusfile in full.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). Op in full.

Spell

A language/runtime adapter (e.g. go, md) that maps generic targets onto a toolchain's real commands. Spell in full.

Module

A magus stdlib namespace a magusfile imports for host capabilities: filesystem, exec, vcs, and more. Module in full.

Buzz

The language magusfiles are written in (the .buzz engine). Buzz in full.

Cache

The content-addressed store magus consults before running a target, so unchanged work is skipped. Cache in full.

CI

An ordinary target you compose yourself with ctx.needs; magus does not hardcode its stages. It is the anchor magus affected ci keys off, and ci strips the rw charm. CI in full.

Queued

A target that wants a slot while the pool is full; it blocks first-in-first-out until a slot frees. The dashboard colors a sample with queued > 0 accordingly. Queued in full.

Agent

A model working through an agent host such as Claude Code, Codex, Cursor or OpenCode. magus treats an agent as one more caller of its CLI, MCP tools and console, held to the same checks as a person. Agent in full.

Tool

An action a model can call through its host: a name, a description, a typed input and a result. magus serves its tools over MCP, each a read of the workspace model or a run of the CLI it already has. Tool in full.

Harness

The deterministic scaffolding around the work on a change, for a person or an agent: compiler errors, formatters, linters, checks, gates, leases and hooks, placed at points a person chose. Around an agent's loop, magus supplies one and keeps it outside. Harness in full.

Skill

A packaged set of instructions an agent host loads when a task matches its description: a directory holding a SKILL.md. magus ships its magus-* skills in the binary and writes them with magus agent install. Skill in full.

Session

An agent host's conversation, by the id the host delivers to its hooks. magus never mints one: a record with no session is unattributed, and the OS user it carries says whose account ran it. Session in full.

Window

The terminal a command runs in. It keys fire-once notices for a caller no host gave a session, and is never recorded as a session. Window in full.

Dispose

The human act of closing an attention request: a judgment rendered, recorded with who and why. Distinct from resolving a review thread or a merge conflict: a disposition answers a request; it does not merge anything. Dispose in full.

Job

The unit of delegated work, and one row of the job store: what an orchestrating agent handed out, with its goal, the checkpoint it was cut against, the paths it may write or must not touch, and the one check it runs. A job's holder is either a session, for work an orchestrator handed out, or the daemon, for its own maintenance. The store records; the agent guard is what reads those facts back when grading a write. Job in full.

Run

One target executing under one magus invocation, such as magus run test web or magus affected ci. A run keeps its captured output behind an output reference. Every magus run is a run whether or not any job asked for it; see Job for how the two relate. Run in full.

Lease

The grant a holder takes on a job: the write and read paths that job declared, enforced in the checkout that took it with magus job exec. A job is the piece of work; a lease is permission over it. Lease in full.

Advisor

One read-only check from the advice suite: it reads the changeset through magus and writes one titled section of findings. The same advisors run as a pull request comment in CI and inside magus diff --impact locally. Advisor in full.

Conventions

This page uses none of the site's convention markers. The full set is on the conventions page.