Skip to content

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_intent
  • chart_resize
  • vessel_mutation
  1. You ask for something — a user intent
  2. The client sends a ChartRequest to the engine
  3. The engine runs an LLM to pick actions and a layout
  4. The engine returns a MaelstromResponse
  5. The client applies the new layout (set_layout)
  6. The client runs the executionPlan (execute_actions)
  7. A ResizeObserver measures the result in grid cells
  8. 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 of H/ V splits. The ResizeObserver is 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