Architecture Overview
Maelstrom turns a user's intent into a laid-out UI by running a loop between
your browser and an LLM on the server. Browser packages describe the current
state and capture input, the server engine decides what to show,
and protocol is the single contract both sides speak. The diagram
below traces one trip around that loop — press play, or explore a package.
You ask for something — a user intent
A reason to update
Something changes on the browser side. The client batches update reasons before it asks the engine for anything.
user_intentchart_resizevessel_mutation
- You ask for something — a user intent
- The client sends a ChartRequest to the engine
- The engine runs an LLM to pick actions and a layout
- The engine returns a MaelstromResponse
- The client applies the new layout (set_layout)
- The client runs the executionPlan (execute_actions)
- A ResizeObserver measures the result in grid cells
- Changed cells become a chart_resize — the loop repeats
The round-trip, in words
It starts with a reason
Every update begins with an UpdateReason, and there are three: a
user_intent (someone asks for something), a
chart_resize (the available grid changed), or a
vessel_mutation (an Instance changed its active Variant).
@maelstrom-co/client
batches the pending reasons and builds a ChartRequest — a snapshot
of the current Vessel instances, the chart's dimensions in grid cells,
the registered Vessels, and the layout mode — then sends it across the
deployment boundary.
The engine decides
@maelstrom-co/engine holds
no layout state — the client owns that — and keeps prior turns only when you
configure a history store. It assembles a prompt, calls an LLM, and returns a
MaelstromResponse with four parts: a userMessage
(the conversational reply), a rationale (its private reasoning),
an executionPlan (the actions to run), and a new
layout (where everything goes).
The client applies it
The client applies the response in a fixed order — layout first, then
actions. set_layout positions and sizes every instance;
execute_actions then runs the executionPlan. The
order is load-bearing: an action can flip a Vessel to a different variant via
transitionsTo, and each variant declares its own
minSize, so positions must be set before actions change them.
The loop closes
Applying the response re-renders the UI. The Chart's
ResizeObserver measures the result and floors it to whole grid
cells. If those cells changed, that is a new chart_resize reason —
and the round-trip runs again. That observer, quietly closing the loop, is what
lets the layout settle. There is no separate overflow pass; the grid is simply
clipped with CSS.
The vocabulary
- Vessel
- A registered UI definition — an id, a description, and a set of Variants. The LLM reasons about Vessels by their descriptions. See the vessel state machine.
- Variant
-
One discrete state of a Vessel, carrying its props, the Actions available in
that Variant, and a
minSize— the smallest number of grid cells it needs. - Action
-
An operation the engine can invoke on an instance. An action may move the
instance to another variant via
transitionsTo. - Chart
-
The surface the Vessels live in. Never pixels: in grid mode the engine
works in integer cells (
rows×columns), and in tiling mode — the default — in a preorder tree ofH/Vsplits. TheResizeObserveris what converts either one to pixels.
The core pieces: protocol is the contract;
client orchestrates and react binds it to React, with
the demo app consuming them on the browser side; and
engine runs the decision on the server.
Where to go next
- Quick start — build a chart.
- Core concepts — Vessels, variants, and the chart.
- The protocol — the message contract in full.
- The engine — how the LLM decision is made.
- Why Maelstrom — the motivation.