Skip to content

feat(runtime): author rich Markdown blocks as JSX via rsc-markdown-stream - #344

Merged
ScriptedAlchemy merged 17 commits into
mainfrom
feat/rsc-markdown-stream
Sep 4, 2026
Merged

feat(runtime): author rich Markdown blocks as JSX via rsc-markdown-stream#344
ScriptedAlchemy merged 17 commits into
mainfrom
feat/rsc-markdown-stream

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Vendors rsc-markdown-stream into this repository as a new workspace package, packages/rsc-markdown-stream, copied from upstream commit eba2ea0b930493b80b9f4f9bb2c582041b0a3f47: the upstream ESM sources and hand-written declarations under src/, built with Rslib to dist/ like the other publishable packages, the node:test suite ported to rstest (57/57 tests kept; the react-server child-process test runs in the integration pool). The name is unpublished on npm, so it is a publishable package of this repo (Apache-2.0 like its siblings, publishConfig.access: public; the upstream MIT notice is preserved as UPSTREAM-LICENSE and listed in NOTICE). @agent-bundle/runtime depends on it as "rsc-markdown-stream": "^0.1.0" — satisfied from the workspace through a pnpm overrides entry, the same form agent-bundle uses for its runtime peer, so the packed manifest carries a plain semver range and no workspace: or git specifier — and re-exports renderToMarkdown / renderToMarkdownStream and the async MarkdownContent component, which renders JSX children — headings, lists, GFM tables, task lists, nested async components, escaped text — to one Markdown string lowered into Agent.Markdown. Root build/typecheck/preview:publish, the license sync, AGENTS.md, and the changeset README enumerate the new package.
  • agent-bundle build: bundle provenance whitelisted only the project's direct dependencies' real paths, so a workspace package linked into a project that itself links another workspace package (@agent-bundle/runtimersc-markdown-stream) failed with AB5000 ("outside output root"). declaredDependencyRoots now follows linked roots' runtime dependencies transitively (registry packages resolve beneath node_modules and are never walked); regression test in build.test.ts reproduces the failure on the old code.
  • Tarball consumer proofs (test:packed): the runtime tarball now depends on rsc-markdown-stream by version, which the registry cannot serve until this repo publishes it, so the shared pack also packs rsc-markdown-stream and offers it in the same npm install as the runtime tarball (npm dedupes the runtime's edge onto it); scaffolded projects take it with --no-save so their manifests stay exactly as the scaffolder wrote them.
  • The audiobook-curator example now authors its DataList / FileList primitives as JSX (byte-identical output — the exact-document pin in cli-dispatch.test.ts passes unchanged) and adds a JSX-authored measured-files GFM table to the duplicate analysis, something the hand-concatenated string approach never supported.
  • Fixed a real bug this surfaced upstream (755c5ee): the react-server condition's React build only exports __SERVER_INTERNALS_*, and rspack's strict ESM linking failed the artifact flight build on the static client-internals namespace access. The internals slot is now located with computed access across client / react-server / legacy keys, with a child-process regression test under --conditions react-server (now packages/rsc-markdown-stream/tests/react-server.test.ts).
  • Full fidelity review of the renderer, fixed upstream in eba2ea0 (the vendored commit). Each case was confirmed by rendering the Markdown back through micromark + GFM before the fix: <br> at a paragraph edge emitted a literal trailing backslash; entity-looking text (&lt;, &#169;) was decoded by parsers; two sibling <ul>s (or <ol>s) merged into one loose list; a task checkbox glued to its label wasn't a task item; <ol start={0}> started at 1; a heading ending in # lost it to the ATX closing-sequence rule; <dl>/<dt>/<dd> glued into one paragraph; <caption> was dropped; <style>/<script>/head metadata leaked as paragraphs; <em> inside <em> became strong; <Activity>/<ViewTransition> threw. Intraword _ (snake_case) is no longer escaped since it can never open emphasis.
  • Docs: MarkdownContent / renderToMarkdown / renderToMarkdownStream documented in the MCP authoring guide and the audiobook-curator example page (en + zh); the preview-packages page lists the new package. website/tsconfig.typedoc.json maps rsc-markdown-stream to its source declarations, since the docs job compiles the runtime sources without pnpm build (this was the one red check on the first push).

Publishing note

rsc-markdown-stream@0.1.0 does not exist on npm yet. Until the first release from this repo publishes it, installing an @agent-bundle/runtime tarball outside this workspace resolves rsc-markdown-stream@^0.1.0 from the registry and 404s — which is why the packed proofs pack it alongside. pkg.pr.new previews are unaffected (preview:publish now includes the package and rewrites the range to the paired preview).

Piped CLI output (real artifact)

### Duplicate analysis: /shared title

- **Candidate files:** 2
- **Measured files:** 2
- **Reclaimable bytes:** 4

| File | Bytes | Status |
| --- | --- | --- |
| /tmp/.../library/Shared title.flac | 15 | measured |
| /tmp/.../library/Shared title.mp3 | 4 | measured |

Test plan

  • packages/rsc-markdown-stream/tests/**: the upstream suite ported to rstest — 57/57 (render, components, fidelity, robustness in the unit pool; react-server.test.ts spawns Node with --conditions react-server in the integration pool); tests/types.ts compile-only check under the package typecheck
  • packages/rsc-runtime/tests/markdown-content.test.ts: renders every supported element (h1–h6, emphasis/strike/code/link/image, nested + ordered + task lists, fenced code, blockquote, aligned table with caption and escaped pipes, hr, container passthrough) through MarkdownContent, pins the exact Markdown, and round-trips it through micromark + GFM (the parser under the Workbench's react-markdown/remark-gfm) asserting the parsed structure; hostile text (*stars*, [x], <tag>, &amp;, ~~, line-start -/#/1./>/---) parses back as literal text; sibling lists stay separate, edge <br> drops, trailing # survives; async components + React.use; custom serializers; streaming parity — 10/10
  • packages/rsc-runtime/tests/markdown-content-flight.test.ts (integration pool): spawns the built package under --conditions=react-server, renders MarkdownContent (with async row components) inside a real Flight request with rsc-markdown-stream resolved as an external dependency, decodes the wire into an Agent Document and asserts the lowered markdown node text
  • packages/agent-bundle/tests/build.test.ts › "bundles symlinked workspace dependencies, transitively, without attributing them as project sources", "excludes a transitive workspace dependency hoisted to an ancestor node_modules", "keeps the project sources when a linked dependency depends back on the project", "excludes linked dependencies that live inside the project directory": the first two fail on the previous declaredDependencyRoots with the exact AB5000 message, the third silently loses src/local.ts from sourceInputs, and the fourth (on c599f78) wrongly reports packages/linked-a/index.js and packages/linked-b/index.js as project sources; all pass with the fix (30/30 in the file)
  • Packed proofs via scripts/run-packed-tests.mjs: packed-stdio-projection.test.ts, scaffold-packed.e2e.test.ts, and (--release) scaffold-packed-matrix.e2e.test.ts — 5/5, installing the runtime tarball together with the rsc-markdown-stream tarball
  • pnpm pack / npm pack of packages/rsc-runtime: packed dependencies show "rsc-markdown-stream": "^0.1.0" — no workspace:, no git URL; pnpm-lock.yaml has no github.com/...rsc-markdown-stream tarball entry (link:../rsc-markdown-stream only)
  • examples/audiobook-curator pnpm check: validate, build --output artifact (flight bundle compiles the workspace renderer under the react-server condition), typecheck, 40 unit + 31 route-unit tests including the byte-exact piped-Markdown pin and the GFM table assertions
  • Workspace pnpm build (four publint passes, rsc-markdown-stream first), pnpm typecheck (now includes the new package's tsconfig), pnpm lint, pnpm test:unit (238 files, 3420 passed), pnpm docs:site:build (language parity clean); branch merged up to origin/main

Self-review

Reviewer: gpt-5.6-sol-medium (change-risk-reviewer), against the diff vs origin/main.

  • Finding 1 — hoisted transitive workspace links were missed (declaredDependencyRoots probed only <packageRoot>/node_modules/<name>, so a dependency B hoisted to an ancestor node_modules by npm/Yarn/hoisting pnpm was resolved by Node and Rspack but skipped by the walk, and AB5000 fired again): fixed in c599f78 — each dependency's manifest is now resolved with createRequire(join(packageRoot, 'package.json')).resolve(${name}/package.json), with an ancestor node_modules walk (up to the filesystem root) as the fallback when an exports map hides package.json; the resolved package directory is then realpathed. Regression test: build.test.ts › "excludes a transitive workspace dependency hoisted to an ancestor node_modules".
  • Finding 2 — a dependency cycle could register the project root itself as an ignored root (project A → linked B → A added A to roots, and collectBundledOutputEvidence then dropped every authored module from provenance without an error): fixed in c599f78 — the project root is canonicalized once and neither it nor any path inside it is ever inserted into roots; the visited set still bounds recursion. Regression test: build.test.ts › "keeps the project sources when a linked dependency depends back on the project" (asserts the implicit src/local.ts import stays in sourceInputs).
  • No findings: runtime compatibility; registry/bootstrap and release ordering (rsc-markdown-stream publishes before @agent-bundle/runtime consumes ^0.1.0); bumpVersionsWithWorkspaceProtocolOnly with the ^0.1.0 range; pkg.pr.new preview; react-server export maps; vendored layout assumptions; docs/changeset; Apache/MIT NOTICE handling.

Second pass (reviewer gpt-5.6-sol-medium, change-risk-reviewer, against c599f78):

  • Warning — nested linked dependencies were treated as project source: isInsideOrEqual(projectRoot, root) rejected not only the project root but every linked dependency resolved to a directory beneath it (a root package linking <project>/packages/dep, or file:./vendor/dep), so those dependency modules stayed attributed as project sourceInputs. Fixed in 3ed48c6 — the check is now canonical-path equality, root === projectRoot, both sides realpathed (the project root keeps its ENOENT fallback); nested roots are inserted again, the project root itself never is. Regression test: build.test.ts › "excludes linked dependencies that live inside the project directory" (fails on c599f78 with the two nested dependency files present in sourceInputs).
  • Note — docs/preview-packages.md still said three packages: fixed in 3ed48c6 — the count is four, both install blocks show the rsc-markdown-stream URL, the documented preview:publish command lists ./packages/rsc-markdown-stream, and the text records that pkg-pr-new rewrites @agent-bundle/runtime's rsc-markdown-stream dependency to the same-sha preview tarball (pkg-pr-new hijackDeps rewrites any dependencies entry naming a co-published package). The workflow needed no change: .github/workflows/package-preview.yml runs pnpm preview:publish, whose script already included the renderer. website/docs/{en,zh}/guide/distribution/preview-packages.mdx had the same "three" and were corrected together; pnpm docs:site:build passes with language parity.

…ream

Re-export renderToMarkdown/renderToMarkdownStream and add the async
MarkdownContent component, which renders JSX children (GFM tables, task
lists, nested async components, escaped text) to one Markdown string
lowered into Agent.Markdown. The audiobook-curator example authors its
DataList/FileList primitives as JSX with byte-identical output and adds a
JSX-authored measured-files table to the duplicate analysis.
@changeset-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3e4b52d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@agent-bundle/runtime Patch
rsc-markdown-stream Patch
agent-bundle Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T00:30:57.679330Z 30befb4 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Sep 3, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@344
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@344
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@344
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@344

commit: 3e4b52d

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

REVIEW ONLY — do not merge. This PR is intentionally experimental and stays open by owner decision; nothing below is a merge signal.

Findings from a full review of the diff (merge-sweep lane, reviewed against the runtime's element/lowering architecture):

Sound:

  • MarkdownContent composes correctly with the existing architecture: it is an async server component that resolves to an Agent.Markdown element, so the lowerers/reconciler handle it through the standard function-component unwrap and thenable materialization paths. No new protocol element, no changes to the Agent.* vocabulary.
  • The trailing-newline contract (replace(/\n+$/u, ''), projections own blank-line joining) is explicit, documented, and pinned by a test.
  • Tests assert concrete behavior: GFM table from async row components, task-list rendering, Markdown punctuation escaping, and the Agent.Markdown lowering shape. The audiobook example's exact-document pin in the streaming test extends (not replaces) existing assertions, which corroborates the byte-identical DataList/FileList claim.
  • The rsc-markdown-stream dependency is commit-pinned via git URL with a lockfile integrity hash, and its react ^19.0.0 peer resolves cleanly against the workspace's 19.2.8. Changeset present (minor bump).

Worth watching before any future merge:

  • A git-URL dependency in a published package's dependencies means npm consumers install from GitHub (codeload). It is reproducible (commit pin + integrity hash) but bypasses the npm registry; consider publishing rsc-markdown-stream to npm before this graduates from experimental.
  • When this eventually rebases, note main has since landed the guard-catalog consolidation (refactor: collapse duplicated guards and helpers onto canonical modules #348); no file overlap today, but packages/rsc-runtime/src/index.ts is a shared touch point.

Local gates were started but intentionally aborted when the PR was marked experimental; no gate verdict is claimed here.

ScriptedAlchemy and others added 16 commits September 3, 2026 04:47
# Conflicts:
#	packages/rsc-runtime/package.json
…to end

Bumps the commit pin to eba2ea0, which fixes output a GFM parser read
differently from the authored tree (edge <br> as a literal backslash,
sibling lists merging, entity-like text decoding, task checkboxes glued
to labels, trailing # in headings, dl/dt/dd gluing, dropped captions,
leaked style/script bodies, em-in-em becoming strong, Activity throwing).

Tests now describe what a reader sees rather than which characters were
emitted: the MarkdownContent suite renders every supported element and
round-trips the result through micromark + GFM (the parser under the
Workbench's react-markdown/remark-gfm), and a new integration test runs
MarkdownContent through a real react-server Flight render of the built
package, decoding the wire back into an Agent Document.
`rsc-markdown-stream` is not published to npm, and npm 12 refuses git
dependencies by default (`allow-git=none` -> EALLOWGIT), so a git-pinned
entry in `@agent-bundle/runtime`'s `dependencies` would fail every
consumer's install -- the AB7015 class the prepack gate rejects. Move the
pin to `devDependencies` so Rslib's `autoExternal` inlines it into
`dist/index.js` (react stays external), and declare the public Markdown
types in `markdown-content.ts` so no emitted `.d.ts` imports a module
consumers never install; the typed assignments of `renderToMarkdown` /
`renderToMarkdownStream` stop compiling if the pinned upstream drifts.

The changeset is now a `patch` (pre-1.0: features are patches) and ends
with the PR reference.
Add "Rich Markdown blocks as JSX" to the MCP authoring guide (en + zh):
what `MarkdownContent` serializes, escaping, the no-trailing-newline
contract, custom `components` serializers, and the exported
`renderToMarkdown` / `renderToMarkdownStream`. The audiobook-curator
example page notes that its report primitives author Markdown this way.
Copy ScriptedAlchemy/rsc-markdown-stream at eba2ea0b930493b80b9f4f9bb2c582041b0a3f47
into packages/rsc-markdown-stream: the upstream ESM sources and hand-written
declarations under src/, built with Rslib to dist/ like the other publishable
packages, with the node:test suite ported to rstest (57 tests; the
react-server child-process test runs in the integration pool). The package is
unpublished on npm, so it is a publishable package of this repository:
Apache-2.0 like its siblings, the upstream MIT notice preserved as
UPSTREAM-LICENSE and listed in NOTICE.

@agent-bundle/runtime depends on it as ^0.1.0, satisfied from the workspace
through the pnpm override (a published manifest never carries workspace:),
and its re-exports resolve to the package again. Root build, typecheck,
preview:publish, and the license sync enumerate the new package.
declaredDependencyRoots whitelisted only the project's direct dependencies'
real paths. A workspace package linked into the project that itself links
another workspace package (@agent-bundle/runtime -> rsc-markdown-stream)
surfaced that second package's modules at a real path with no node_modules
segment, and `agent-bundle build` rejected them as sources outside the
output root (AB5000). Walk each linked root's runtime dependencies too;
registry packages resolve beneath node_modules and are never walked.
The runtime tarball now depends on rsc-markdown-stream by version, which the
registry cannot serve until this repository publishes it. Pack it with the
other run-level tarballs and offer it in the same npm install as the runtime
tarball (npm dedupes the runtime's edge onto it); scaffolded projects take it
with --no-save so their manifests stay exactly as the scaffolder wrote them.
# Conflicts:
#	website/docs/en/guide/authoring/mcp.mdx
#	website/docs/zh/guide/authoring/mcp.mdx
The docs build compiles packages/rsc-runtime/src without pnpm build, and the
renderer publishes its types from dist, so TypeDoc could not find the module
in CI. Map it to the hand-written src/index.d.ts like the runtime entries.
…e the project root

The provenance walk probed only <packageRoot>/node_modules/<name>, so a
transitive workspace dependency hoisted to an ancestor node_modules (npm,
Yarn, pnpm with a hoist pattern) was missed and AB5000 fired. It also added
a resolved root before checking whether it was the project itself, so a
dependency cycle back onto the project (A -> B -> A) silently dropped every
authored module from sourceInputs.

Resolve each dependency's manifest with createRequire(...).resolve(), fall
back to an ancestor node_modules walk when an exports map hides
package.json, canonicalize the project root once, and never register it or
anything inside it as an ignored root.
isInsideOrEqual(projectRoot, root) also rejected every linked dependency
that lives beneath the project (a root package linking <project>/packages/dep,
or file:./vendor/dep), so those dependency files stayed attributed as project
sources. The contract is equality: compare the realpath'ed dependency root
with the canonical project root, so nested roots are excluded again while the
project root itself never is.

docs/preview-packages.md and the website preview pages now count four
publishable packages, show the rsc-markdown-stream install URL, document the
full preview:publish command, and state that pkg-pr-new rewrites the runtime's
rsc-markdown-stream dependency to the same-sha preview tarball.
@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) September 4, 2026 21:59
@ScriptedAlchemy
ScriptedAlchemy merged commit 2e91ea1 into main Sep 4, 2026
14 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the feat/rsc-markdown-stream branch September 4, 2026 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant