Skip to content

9. Multi-substrate and the agent loop

You have learned Story as a human workshop. This chapter steps back and shows why the same data-shaped artifact also works for renderer experiments, MCP tools, and coding agents. The practical takeaway is simple: Story's UI, test runner, static docs, and agent surface are meant to read the same registry and run the same variants.

Two hosts

An agent drives Story in one of two hosts, and each host owns its own frames.

  • The story-mcp server is a stdio process in its own JVM. It sees the stories loaded into that JVM, runs variants in frames it allocates there, and has no bridge to a browser. A browser-only read such as list-substrates or read-a11y-violations therefore answers with a capability-unavailable error, never an empty result.
  • The browser is the app you have open. An agent reaches its Story registry through re-frame2-pair's eval-cljs, then reads, dispatches to and traces a variant with the ordinary pair tools, because a variant is a frame.

A variant id registered in both hosts names two frames with two separate app-dbs, so run a whole loop in the host that holds the frame you care about. One rule picks the host before you start, and both skills carry it word for word beside the promote, fidelity-upgrade and explain recipes: the re-frame2 skill for story-mcp in story-mcp-loop.md, the re-frame2-pair skill for the browser in stories.md. The story-mcp README states the same split, and Two surfaces, one live door covers the boundary in more depth.

Substrates

A Story body names a view id and application behaviour. It does not contain JSX, Reagent hiccup, or UIx component code. That keeps the variant body independent from the renderer:

(rf.story/reg-story :story.login
  {:component  :my-app.views/login-card
   :args       {:heading "Sign in"}
   :substrates #{:reagent}})

The tutorial and the scaffolded path are Reagent-focused because that is where most readers start, but the substrate set is not a Reagent set. A member of :substrates names which registered render fn embeds the subject — the authoring layer — and not which adapter rf/init! installed. :fresco is the member that makes the difference visible: a deck declaring :substrates #{:fresco} runs in a Reagent-hosted shell — the installed adapter is Reagent's, the authoring is Fresco's — resolves its own frame through React context, and responds to writes into it. Fresco does ship an adapter of its own, so the two spellings coincide often enough to be worth separating out loud: which adapter is installed is a different question from which render fn embeds the subject. There is a worked one at tools/story/testbeds/fresco_counter/, and it rides the same PR-path play gate every Reagent deck does.

Story installs the :reagent render fn itself and leaves :uix and :fresco to the host application, because each one's only dependency is the host's — five lines at boot, and Story core never names them. The design reason is still worth understanding: a variant should describe a state and behaviour, not smuggle a renderer-specific render function into the artifact.

When a substrate cannot render a variant, Story should say that. This is the same honesty rule as :cannot-run: do not pretend the tool proved or displayed something it could not actually run.

Story-MCP

Story-MCP is a separate tool artifact. Story core owns the registry, variants, runtime, snapshot identity, and shell. Story-MCP owns the MCP server, JSON-RPC transport, tool registry, and wire redaction/elision.

The canonical tool list currently includes:

Category Tools
Dev get-story-instructions, preview-variant, list-substrates
Docs/read list-stories, get-story, get-variant, list-tags, list-modes, list-decorators, list-assertions, variant->edn, get-docs-markdown, explain-variant
Testing run-variant, snapshot-identity, read-a11y-violations, read-failures
Write, gated register-variant, unregister-variant

That is the agent-facing version of the same operations a human performs in the shell: list states, preview one, run it, read failures, explain how it was assembled, record a useful interaction, and optionally write a variant back through the gated authoring surface.

Skills

The repo also carries Story-related skill documentation under skills/. Those skills are not a second Story model. They are operating instructions for agents using the same Story and Story-MCP surfaces.

The useful loop, run inside one of the two hosts above, is:

  1. list or get the variant;
  2. preview it if needed;
  3. run it;
  4. read failures;
  5. in the browser host, inspect the same frame/epochs through pair or Xray tooling;
  6. record or register a refined variant when writes are explicitly allowed.

That loop is only good if it mirrors the human loop. If the agent sees a different artifact from the one the user sees in the Story shell, the tool has split its own truth and should be fixed.

Why this matters

The boring implementation detail is also the product point: a variant is a data-shaped artifact in a registry.

Because of that, the same variant can be:

  • rendered in Canvas mode;
  • placed in a workspace grid;
  • documented in Docs mode;
  • run in Test mode;
  • executed by rf.story/run or rf.story/is;
  • inspected through Xray;
  • shared through URL or EDN;
  • addressed by Story-MCP.

That is the reason Story can aspire to more than "Storybook, but in Clojure". Storybook is excellent at component examples. Story can use re-frame2's frames, schemas, effects, machines, trace bus, and Xray to make examples, tests, documentation, diagnostics, and agent workflows converge on one artifact.

There is still work to do on polish and ecosystem. Storybook has years of mindshare and a gigantic addon world. Story should not try to win by adding every addon-shaped idea as a new subsystem. It should win by making the core workflow cleaner: name the state, render it honestly, test it cheaply, diagnose it with evidence, and let humans and agents operate on the same thing.

Useful references