Skip to content

RFC: framework mode — structure in config and conventions, JSX only for rendering #63

Description

@ScriptedAlchemy

Summary

Framework mode: structure lives in agent-bundle.config.ts and file conventions; the RSC/JSX layer is reserved for rendering. Declaring a <Skill>, an <McpServer>, or targets in a JSX tree becomes a nudged legacy pattern (AB473x informational diagnostics, never errors); the JSX that stays is the JSX that renders — MCP/Hook result trees at runtime, and skill bodies compiled to markdown at build time.

This is the sibling of RFC #50 (build/entry conventions): #50 made agent-bundle own the build through one config plus entry conventions; this RFC makes the same config-plus-conventions layer own the structure, and gives the RSC layer a single, teachable job.

All file:line references are at 1286f5b2 (current main).

The owner's direction

From the owner's design note, quoted in substance:

Audiobook-curator shouldn't have an agent-bundle JSX component, or define targets in JSX — targets would be defined in the agent-bundle config. We don't have a "skill" that we import: in framework mode it would just be a skills directory, or whatever convention we use for this — not declaring a <Skill> component in the JSX. JSX is for when I want to render something — the output of a skill, more like rendering a route: there's a compile-to-markdown step; whether it's a hook or a tool, we'd have the option of bundling that.

And the follow-up constraint:

Config and file conventions cannot be cumbersome or overly complicated to learn. Use Rspress as a reference; react-router rsbuild plugin for inspiration if needed.

Where the code is today

The JSX structural layer duplicates the config layer. defineRscAgentBundle (packages/rsc-runtime/src/plugin-definition.ts:168-252) walks a tree of <AgentBundle targets=…> / <Skill source=…> / <Script> / <McpServer> / <Operation> elements (plugin-elements.ts:45-56) and lowers it to… an AgentBundleConfig. Every structural prop it accepts is a field the config file already expresses (packages/agent-bundle/src/core/types.ts:164-180): targets, skills, scripts, mcp.servers, marketplace, runtime.node. The JSX tree is a second syntax for the same declarations, with its own validators re-implementing the config validators.

The conventions layer already half-exists. When config is silent, discoverProject already auto-discovers skills/*/SKILL.md (config/discover.ts:92-101) and normalization already marks those skills provenance.kind: 'conventional' (config/normalize.ts:601-607) — the enum was built for this (core/types.ts:183). RFC #50 added src/cli.ts, src/index.ts, and src/mcp/<server-id>.ts with the same rule, stated once in docs/entry-conventions.md:49:

Conventions fill the config when it is silent; config always wins.

The rendering layer is the part worth keeping. Mcp.Result/Mcp.Text/Hook.Result elements (rsc-runtime/src/elements.ts) rendered per operation result and lowered on the wire (mcp-server.ts:32-35, lower-mcp.ts, lower-hook.ts) are genuinely good: typed results rendered as content. That is "JSX renders something," and it stays untouched.

The three consumers already vote:

What a newcomer must learn (the gate)

Framework mode is allowed exactly three concepts. If a design draft needs a fourth, cut or merge.

# Concept Learned from
1 One directory convention: skills/<name>/SKILL.md just works Making a folder (Rspress: files in docs/ are the structure)
2 One config file: agent-bundle.config.ts, small flat surface (plugin, targets, mcp.servers, scripts; skills/bin/lib only to override) One screen of defineConfig (Rspress: one rspress.config.ts)
3 One mental rule: JSX = rendering (results at runtime, skill bodies at build) One sentence

The single precedence rule is the one already shipped and is repeated, not amended: conventions fill the config when it is silent; config always wins. No second way to declare structure — the JSX structural elements stop being taught (kept working, nudged, below).

Rspress layering is the reference: newbie path = files in a folder + tiny config; power features (_meta.json ↔ explicit skills:/bin:/lib: overrides; plugins/themes ↔ rendered skills and the tools hatch) stay discoverable but never appear on the newcomer path. From the react-router rsbuild plugin we take the provided-default trick (a missing file gets a sensible default, not an error): a declared server with no entry falls back to src/mcp/<id>.ts (shipped), a silent config falls back to the skills directory (shipped).

Design

(a) Structure: config or convention, never JSX

Targets, servers, scripts, and skills are declared in agent-bundle.config.ts, or discovered by convention where a convention exists:

  • skills/<name>/SKILL.md is formalized as THE skills convention (it already ships; this RFC promotes it to documented contract). No skills: field on the newcomer path.
  • Explicit skills: remains the override for out-of-tree paths and globs. When explicit config leaves a conventional skills/*/SKILL.md directory uncovered, source validation reports a new AB4734 informational shadow nudge — the exact shape of AB4731/AB4732/AB4733 (config/validate.ts:43-54, docs/diagnostics.md §Migration nudges).
  • Servers/scripts/bin/lib: unchanged from RFC RFC: agent-bundle owns the build — one config, framework entry conventions #50; no new concepts.

(b) JSX = rendering

Two rendering surfaces, one existing and one new:

  1. Runtime result rendering (the keeper, unchanged): operations render results as Mcp.* / Hook.* trees; createRscMcpServer lowers them per call. Nothing changes.
  2. Build-time rendered skills (new, the power tier — never required): a skill directory may provide skills/<name>/SKILL.tsx instead of SKILL.md. The module default-exports a component (sync or async) and exports a frontmatter record; at discovery/build time the framework loads it and compiles the tree to markdown, emitting the same skills/<name>/SKILL.md artifact byte-layout every host already consumes. This is "rendering a skill like rendering a route": the compile-to-markdown step from the owner's note.

The compile-to-markdown implementation is deliberately the minimal honest version, not a general HTML-to-markdown engine:

  • The module loads through the same jiti pipeline that already executes consumer TSX at config time (config/load.ts:68-80jsx: { runtime: 'automatic' }), so this adds no new execution model.
  • The renderer resolves function components (awaiting async ones) and hand-emits markdown for a documented element subset: h1–h6, p, ul/ol/li (nested), strong/b, em/i, code, pre (fenced, language-* class), blockquote, a, hr, br, fragments, strings/numbers. Anything else is an error diagnostic naming the element (new AB3003–AB3005 in the skill-parsing family). No renderToStaticMarkup+turndown dependency chain; limits are documented, and the escape hatch is writing SKILL.md by hand.
  • When both SKILL.md and SKILL.tsx exist, SKILL.md wins (a file the author wrote beats a file the framework would generate — same rule as config beating convention) with an AB4736 shadow nudge.
  • Emitted rendered skills flow through the existing artifact validation (build/validate-artifact-skills.ts) — same frontmatter contract, same name-matches-directory rule, same resource-reference checks.

(c) Structural elements: deprecation path, not removal

<AgentBundle>/<Skill>/<Script>/<McpServer> keep working byte-for-byte. defineRscAgentBundle brands the config it lowers (a registered symbol, invisible to JSON/digest/artifacts), and source validation reports AB4735, an informational nudge in the AB473x pattern: structure is declared through RSC JSX elements; framework mode declares it in agent-bundle.config.ts (conventions fill when silent) and reserves JSX for rendering. Removal is out of scope for this RFC; a future major can revisit once consumers migrate.

For the runtime side, framework mode gets the missing JSX-free constructor:

// src/application.ts — operations only; no structure
export const application = defineRscApplication({
  plugin: { name: 'audiobook-curator', version: '1.0.0' },
  operations,
});

defineRscApplication performs the same duplicate-id/CLI/tool checks as the element lowering and returns the same RscAgentBundleApplication shape, so createRscMcpServer(application, 'curator') and runRscCli(application, argv) are unchanged call sites. createRscMcpServer learns that when the application carries no mcp config (structure lives in the config file now), a server name is valid when operations project to it.

(d) Migration sketch per consumer

  • audiobook-curator (in this arc, the proof): agent-bundle.config.ts becomes the real config (plugin metadata, targets: ['claude','codex'], marketplace, runtime.node, scripts, mcp.servers.curator: {}); src/mcp-server.ts moves to src/mcp/curator.ts (the RFC RFC: agent-bundle owns the build — one config, framework entry conventions #50 convention fills the entry); the skill is picked up from skills/curate-audiobooks/ by convention; application.tsx shrinks to the operations catalog via defineRscApplication. Behavior-preserving: same compiled skill markdown, same operations, same CLI/MCP projections. The result must read as a newcomer's first real project, not a showcase.
  • skills-starter: already framework mode. Optional cleanup: drop the skills: list (all three live under skills/) and let the convention carry it.
  • movie-library (external): five src/mcp/<id>.ts conventional entries + mcp.servers config; SKILL.md stays the file it already is; the operations catalog moves to defineRscApplication when it adopts the RSC runtime. No JSX required anywhere.

Delivery plan

  1. This arc: AB4734 skills shadow nudge; rendered-skill compile-to-markdown pipeline (SKILL.tsx); AB4735 structural-JSX nudge + config brand; AB4736 rendered-source shadow nudge; defineRscApplication; audiobook-curator migration; docs/framework-mode.md (newcomer path fits one screen — if it can't, the design is wrong and gets iterated, not the prose); diagnostics reference updates; changesets (minor agent-bundle, minor @agent-bundle/rsc-runtime).
  2. Follow-ups: skills-starter convention cleanup; movie-library upstream migration; eventual structural-element retirement (major, separate RFC).

If the rendered-skill pipeline proves disproportionate mid-arc, it ships as a follow-up issue carrying this design, and the rest lands without it.

Relationship to RFC #50

#50 gave agent-bundle the build: one config, bin/lib, entry conventions, the stdio shell, the tools hatch. This RFC completes the other half of the same discipline: the config-plus-conventions layer owns what the plugin is, and the RSC layer owns what the plugin renders. Same precedence sentence, same nudge family, same additive-first migration posture.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions