Architecture
This page is where you find out where a change goes before you write it: which layer of src/hmz/ it belongs in, what that layer may import, and, for the two changes that touch many layers at once, a backend and a command, every file to visit in order.
Before you start
- A checkout set up as in Contributing.
- For a first, small change, Your first patch is the gentler way in.
How the layers work
src/hmz/ is a stack of layers, and code in each layer may import only the layers the table below gives it. Pick a layer to see what it may import and open its SPEC. A test holds the same table, so an import that breaks it fails the run.
tests/integration/test_core_layering.pyhmz.runtime.runnerThe hmz exec line: reading it, finding the flow, checking it, refusing it, running it.
may import
imported by
picked may import only some of its modules inside one of those imports it may not
Where a change goes
| You are changing | Put it in |
|---|---|
| how a coding agent CLI is driven, or its accounts, models, fallbacks or prices | coganchor/ |
| the machine an agent's turns land on | coganchor/machines/, per Machines |
| a type, mixin, hook or error a flow imports | flows/, then check the flows humanfia/flowverse lists still work |
| how a flow runs: the engine, budgets, resuming, the harness and environment drivers | runtime/flowing/, then check those flows too |
the hmz exec line: a flag, a refusal | runtime/runner.py |
| what a run writes down | runtime/epic.py |
| reading a backend's logs back as a trace | runtime/tracing/ |
| what a run looks like as it happens -- who is working, what it has cost, what a side question is told -- for every interface | runtime/watching/, once |
anything two of cli, tui, web and daemon would each need | runtime/doing/, once |
| who is outside a run: claims, away, questions, lines said to agents, asides | runtime/doing/hosting.py |
| a slash command, a key, a menu; what the interface draws of a message | tui/ |
| a view of the web interface, or a route it answers | web/: the page under web/static/, plain ES modules, and its routes beside the server |
| runs kept going after the terminal closes, and the socket frontends reach them over | daemon/ |
| a new command | cli/ |
| the Python API another tool calls | sdk/ |
| a flow Humanize ships | flows/builtin/, for chat and the six loops FlowBench scores. Any other flow lives in a repository of its own, listed in humanfia/flowverse: see Flowverses |
Nothing sits at the top of src/hmz/ but __init__.py and __main__.py. Name a module for what it holds, in the words hmz and these docs use.
The rules the test checks
tests/integration/test_core_layering.py holds the table the diagram draws. The run fails when:
- A module imports what its layer may not. Relative imports count the same as absolute ones.
from hmz.runtime import telemetrynamestelemetry, not the whole ofruntime. - Two layers name each other. One pair may:
flowscalls intoruntime/flowingfrom insideflow,loadandOutworlder.new, and never at import. Importinghmz.flowsmust load nothing else of Humanize buthmz.coganchor.faking, which holdsFakeTool,FakeCliandFakeCommand, and that is checked too. - A top-level package is missing from the table.
cliis the one exemption. - The anchor's target half loads more than it may.
coganchor/serve/runs on the target machine, which may be any architecture, so it may importcoganchor.proto, andcoganchor.fencewith thecoganchor.linuxbindings it is put up with, and nothing else.
To add a layer or an edge, edit ALLOWED in that test. The diagram above copies it, so change docs/.vitepress/theme/components/HmzStack.vue in the same commit.
One rule no test checks: import another layer inside the function that needs it, not at the top of the module, so hmz exec pays only for what it uses. Ruff's PLC0415 is off for this.
Adding a backend
A backend is one coding agent CLI Humanize drives. Adding one touches most layers, so do it in the order below, with a test run after each group. The walk-through follows how MiniMax Code, mcode, was added, in one commit: git show --stat c3c1e6a lists every file it touched.
An ACP CLI needs no code
A CLI that speaks the Agent Client Protocol can be added from the interface, on the Accounts page of /settings.
You need: the CLI installed and signed in, so a system test can drive it; its --help and a turn or two of its own logs, since several steps copy what it really writes; and which kind of driver it needs, which specs/features/agents.md says.
1. Describe it, in coganchor/
A
Profileincoganchor/backends.py: its name andaliases, its home (home_var,home_dir), thelogsone session writes as globs taking{ident}, itsefforts, thesessionspaths a turn keeps,skills,creds, thewaysin, thehostsit must reach, and the line thatinstallsit. Add it toPROFILES.pythonProfile( name="mcode", aliases=("mcode", "minimax", "minimax-code"), home_var="MINIMAX_DATA_DIR", home_dir=".minimax", logs=("v2/sessions/*/*/*/*-session_{ident}/messages.jsonl",), encodes=True, ... )Set
encodes=Truewhen the CLI names a session's files by its id in URL-safe base64 rather than by the id itself, as MiniMax Code does. Whatever looks for a session's logs asksprofile.logged(ident), which answers the globs with the id spelled the way the CLI spells it, and never formatslogsitself.A driver in
coganchor/agents/<name>.py. SubclassCommandSessionBasewhen a turn is one run of a command (mcode execis), orStreamSessionBasewhen it is one long-lived process spoken to a line at a time. Declare on the agent class, ascounts, the kinds of token it reports, named asKINDSincoganchor/agents/event.pynames them.A row of
DRIVENincoganchor/agents/__init__.py, and the driver's classes in its__all__:python"mcode": (MiniMaxCodeAgent, MiniMaxCodeAgentConfig),A way of asking which models it runs: a function in
_READINGincoganchor/models.py.mcode's readsmcode provider list --json.Its state paths: an
AgentProfileincoganchor/statepaths.py.pythonAgentProfile( name="mcode", state_paths=("~/.minimax", "~/.mavis"), ),
2. Offer it to flows, in flows/
A
HarnessKind, a protocol and a row ofHARNESS_AGENTSinflows/agents.py. The protocol carries exactly the mixins the CLI serves, and is exported fromflows/agents.pyandflows/__init__.py:pythonclass HarnessKind(StrEnum): ... MCODE = "mcode" class MiniMaxCodeAgent( Agent, SubagentStartHookAgentMixin, SubagentStopHookAgentMixin, Protocol ): """MiniMax Code, with everything it can do."""Give it a row in the
tests/unit/flows/tests ofHARNESS_AGENTS. A change here also checks the flows humanfia/flowverse lists still work.
3. Read it back, in runtime/tracing/ and runtime/watching/
- A trace reader in
runtime/tracing/readers/<name>.py, and its row inruntime/tracing/collector.py. - The live tally: what its log calls each kind of token, as a row of
_KINDSinruntime/watching/tally.py, and a branch in_spentthere, so the tally moves during a turn. Cover its kinds in thetests/unit/runtime/watching/tests of the tally.
4. Test it
- Unit tests of the driver in
tests/unit/coganchor/, through its public names, with the CLI's process mocked. - A stand-in for the integration tests: a fake of the CLI, its flags read off the real CLI's
--help, andtests/integration/test_agents_<name>.pydriving it. - A system test: its cheapest model in
CHEAPESTintests/system/test_harness_task.py, which sets every installed CLI the same real task.
5. Say so
The docs pages and components that list or count the backends. Search for an existing backend's name and add yours beside it:
shgrep -rln mimo docs --include='*.md' --include='*.vue' --exclude-dir=node_modulesWhere a SPEC lists the backends too, propose the change: do not edit a SPEC unless you were asked to.
Check it worked
uv run pytest tests/unit/coganchor tests/unit/flows tests/unit/runtime/watching \
tests/integration/test_agents_<name>.py tests/integration/test_core_layering.pytests/integration/test_core_layering.py is the one that fails when a step imported across a layer it may not. Then the real thing, which takes minutes and spends tokens:
uv run pytest -n0 tests/system/test_harness_task.py -k <name>Adding a command
- A module under
cli/if it takes a parser of its own, and a thin wrapper incli/__init__.py. - An entry in
COMMANDS. A line Humanize runs for itself, rather than one a person types, goes inINTERNALinstead, whichhmz internalroutes. - Its output through
cli/output.pywhere the command has a--json, so a strayprintnever lands in the stream a program reads. - A terse help. Every flag's help, command summary, description and epilog is thirty words or fewer; the detail goes in CLI reference. A unit test in
tests/unit/cli/reaches every parser throughmainand fails on anything longer. A subcommand a command routes by hand, ashmz internal anchorroutesserve, needs a line in itsROUTES.
Check it with uv run hmz --help, which lists every entry of COMMANDS, and with uv run hmz <command> --help.
SPECs
specs/ states everything Humanize shows to the outside, in MUST, MUST NOT and MAY terms, and nothing about how it is built. specs/api/ holds the interfaces: the flow API, the SDK, the daemon protocol, the HTTP API, the command line, settings.yaml, and every file Humanize may leave on disk. specs/features/ holds one file per feature. specs/README.md holds the rules every SPEC is written by and the terms they share. Pick a layer in the diagram to open the SPEC that governs what it shows. Change the code to match the SPEC, and do not edit a SPEC unless you were asked to. Propose a SPEC change separately. How the layers are built and may name one another is this page's business, not a SPEC's.
Every SPEC
specs/README.md | How a SPEC is written, numbered and cited; the terms every SPEC shares |
specs/api/flows.md | The flow API: what a flow imports, declares and is handed |
specs/api/sdk.md | hmz.sdk: how a program that is not Humanize drives it |
specs/api/daemon.md | The daemon's protocol, as a frontend sees it |
specs/api/http.md | Every HTTP endpoint hmz web serves |
specs/api/cli.md | Every command line |
specs/api/settings.md | Every key of settings.yaml, and every environment variable read |
specs/api/files/home.md | ~/.hmz |
specs/api/files/workspace.md | What Humanize may leave in a workspace, and what never |
specs/api/files/epics.md | An epic's directory, its records, and .hmzepic archives |
specs/api/files/machine.md | Per-user temporary and cache directories |
specs/api/files/harness-homes.md | What Humanize keeps or builds for each CLI's home |
specs/api/files/desktop.md | The macOS app's files |
specs/api/files/targets.md | What Humanize may leave on another machine, container or job |
specs/features/epics.md | Starting, stopping, budgeting and picking up an epic |
specs/features/agents.md | Driving coding agent CLIs: configuration, permissions, faults |
specs/features/accounts.md | Which account an agent runs as |
specs/features/placements.md | Where a CLI runs against where its work is, and mirrors |
specs/features/environments.md | Local, ssh, docker, swarm, Apple container and Slurm envs |
specs/features/chat.md | The chat flow, in every frontend |
specs/features/btw.md | Side questions |
specs/features/monitoring.md | The live monitor, cost, notices, traces and profiles |
specs/features/backups.md | Git backups, export and import |
specs/features/telemetry.md | What is reported, and what never |
specs/features/updates.md | hmz update and update notices |
specs/features/daemons.md | One daemon per machine, and remote daemons |
specs/features/tui.md | Every behaviour the terminal interface must have |
specs/features/web.md | Every behaviour the web interface must have |
specs/features/desktop.md | Humanize Desktop, the macOS app in desktop/: a window on hmz web |
Next steps
- Where a test goes, for which tests a change needs
- Working on these docs, for the pages a change updates
- Reference › Agents, for what each backend does, as users read it