Summary
Replace the current synchronous React-element lowerer with an actual React
Server Components execution path. Agent routes render a transport-neutral Agent
Document rather than HTML. One live internal Flight stream carries async Server
Component and Suspense work to an Agent Document decoder; MCP, CLI, scripts,
Workbench, and tests then project the same document according to their real
capabilities.
This issue owns rendering and projection. Typed request context comes from the
request-context feature, cross-request state comes from the optional state
kernel, and messages for future or different invocations come from the directed
notices feature.
Problem
@agent-bundle/rsc-runtime currently provides a small Mcp.*/Hook.* JSX
vocabulary and synchronous lowerers. The lowerers call function components as
ordinary functions, walk the resulting elements, and produce one completed
protocol object. CLI output bypasses JSX and serializes the validated operation
result directly.
That is a useful React-shaped protocol DSL, but it is not a React renderer or
an RSC/Flight runtime. React does not own component execution, so async Server
Components, Suspense, request cancellation, boundary errors, backpressure, and
incremental reveal are unavailable. This is the implementation gap described
by #88.
Goals
- Run route components through React's RSC/Flight implementation rather than
manually invoking component functions.
- Define one immutable Agent Document vocabulary for agent-facing results.
- Preserve a live Flight stream through decoding and projection.
- Distinguish Suspense boundary replacement from mutable progress updates.
- Produce honest MCP, CLI, script, Workbench, and test projections.
- Pin and test one exact compatible React/RSC package set because framework
integration APIs are not semver-stable across React minors.
Non-goals
- Owning request identity or provider composition; that belongs to the request
context feature.
- Treating React state, context, or
useSyncExternalStore as server-side
persistence.
- Keeping a completed Server Component mounted as a subscription.
- Inventing rich partial MCP tool results where the protocol exposes only
progress notifications and one final result.
- Shipping Server Functions or browser actions in the first renderer release.
The compiler must preserve client/server boundary information, but action
transport is a separate feature.
- Parsing every Markdown string into the core document tree.
Rendering architecture
generated route entry
-> install typed Agent request context
-> render Server Component model to a live Flight stream
-> incrementally decode Flight
-> commit Agent Document snapshots/events
-> target projector
|- MCP progress + final CallToolResult
|- CLI text/json/ndjson
|- rendered script stream
`- Workbench/test projection
The existing examples/rsc-agent-runtime already proves the important
implementation seam: a worker installs request context and writes Flight. Its
caller currently buffers the complete Flight payload before decoding. The
framework implementation keeps this boundary but preserves the stream,
cancellation, and backpressure end to end.
Every renderer invocation is one logical invocation and produces one final
Agent Document. An ordinary MCP tools/call maps to one request and one final
CallToolResult. A task-augmented invocation spans tools/call, tasks/get,
and tasks/result RPCs but resolves to the same final document. A later host
event, state revision, or notice starts a new render; it does not write to a
closed Flight or hook response.
Authoring model
Route modules are async Server Component entrypoints. Data loading can begin
before or during rendering, and React owns suspension and resumption.
import { Suspense } from 'react';
import { Agent } from '@agent-bundle/runtime';
export default function Audit({ input, signal }: ToolRouteProps<typeof schema>) {
const audit = auditLibrary(input.root, { signal });
return (
<Agent.Result>
<Agent.Markdown># Library audit</Agent.Markdown>
<Suspense fallback={<Agent.Progress message="Inspecting files" />}>
<AuditSummary audit={audit} />
</Suspense>
</Agent.Result>
);
}
async function AuditSummary({ audit }: { audit: Promise<AuditReceipt> }) {
const receipt = await audit;
return (
<>
<Agent.Markdown>
Inspected **{receipt.files}** files and found
**{receipt.findings.length}** findings.
</Agent.Markdown>
<Agent.Json value={receipt} />
</>
);
}
Server Components are server-side by convention and do not use a 'use server' directive. 'use server' identifies callable Server Functions, not
Server Components.
Async Server Components use the React-independent request accessor from the
context issue:
const context = await agent();
This renderer adds a narrow synchronous bridge:
const context = useAgent();
useAgent() unwraps one stable request-scoped promise with React use. It is
not a subscription or state container. Application hooks such as
useWorktree() may delegate to it in synchronous Server Components, while
async components should prefer await agent() or an async provider accessor.
Agent Document vocabulary
The initial public vocabulary is deliberately protocol-oriented rather than a
browser DOM:
<Agent.Result metadata={...}>...</Agent.Result>
<Agent.Markdown>...</Agent.Markdown>
<Agent.Text>...</Agent.Text>
<Agent.Json value={...} />
<Agent.Progress completed={35} total={100} message="Analyzing files" />
<Agent.Image data={...} mimeType="image/png" />
<Agent.Audio data={...} mimeType="audio/wav" />
<Agent.Resource uri="audiobooks://catalog" name="Catalog" />
<Agent.Error code="...">...</Agent.Error>
The committed document is a versioned, finite, immutable, serializable value:
interface AgentDocument {
version: 1;
status: 'success' | 'represented-error' | 'failed';
root: AgentDocumentNode;
value?: JsonValue;
}
Nodes contain no functions, class instances, streams, request-context handles,
or host-native objects. Projectors may impose stricter roots—for example, MCP
structured content is object-valued—so an array/scalar value must be wrapped,
rendered as text, or rejected according to an explicit route projection.
Unsupported HTML elements fail clearly. Image, audio, resource, and other rich
content are capability-gated per projector. For example, MCP permits audio
content blocks, but a selected host/model may not consume them. The projector
must use a declared fallback or fail; it never silently drops content or
pretends that transcription occurred.
Agent.Markdown remains a semantic text leaf. A browser-facing projector may
use TanStack Markdown or another audited parser to render that leaf. The core
renderer does not parse Markdown into React only to turn it back into an Agent
Document.
Agent.Progress represents progress in the current document snapshot or a
Suspense fallback; JSX does not create a mutable progress channel by itself.
Repeated progress events come from the request-scoped progress reporter or a
framework-owned streaming producer and follow the render-event contract below.
Render event protocol
type AgentRenderEvent =
| { type: 'shell'; sequence: number; document: AgentDocumentSnapshot }
| { type: 'progress'; sequence: number; completed: number; total?: number; message?: string }
| { type: 'replace'; sequence: number; boundaryId: string; document: AgentDocumentSnapshot }
| { type: 'error'; sequence: number; boundaryId?: string; error: AgentRenderError }
| { type: 'complete'; sequence: number; document: AgentDocumentSnapshot };
Rules:
- boundary identifiers are stable only within one invocation;
complete contains the canonical final document;
- progress is mutable status, while
replace means a suspended subtree became
ready;
- cancellation aborts pending work and closes the stream;
- no event is accepted after
complete;
- depth, node count, bytes, event rate, and elapsed time are bounded;
- renderer, decoder, and projector boundaries propagate real backpressure;
project file/network/command work receives cancellation and only receives
backpressure when its own capability explicitly supports it.
Projection contracts
MCP
The standards-compatible MCP projector:
- emits
notifications/progress only when the caller supplied a progress
token;
- maps only monotonically increasing numeric progress, optional total, and a
short message into that notification;
- buffers shell/boundary replacement internally;
- returns one valid final
CallToolResult with supported content blocks and
optional object-valued structured content.
For an ordinary tools/call, progress is out-of-band and the request resolves
to that final result. For a task-augmented call, the client explicitly opts in,
the server and tool advertise task support, the initial request returns a
CreateTaskResult, and the client retrieves status and the final result through
the Tasks operations. The projector preserves the caller's opaque progress
token; progress remains optional and is emitted only while the ordinary request
or task is active. The token remains valid through a task's terminal state.
Tasks are optional lifecycle and deferred-result retrieval, not a rich
partial-result stream.
For ordinary MCP requests, notifications/cancelled maps into the renderer
AbortSignal. Task-augmented requests use tasks/cancel. Under MCP
2025-11-25, an HTTP SSE disconnect by itself is not cancellation.
A future Agent Bundle MCP extension may negotiate shell/patch delivery, but the
portable projector does not assume it.
CLI and scripts
- interactive TTY output may update progress and placeholders in place;
- piped Markdown emits only stable content and one correct final document;
- JSON returns the canonical final value;
- NDJSON exposes the complete sequence-numbered render event stream;
- diagnostics use stderr; rendered machine output uses stdout;
- a plain JS/TS script keeps ordinary Node stdout/stderr semantics and does not
enter the renderer.
NDJSON render events are an Agent Bundle CLI/script dialect. They are never
written as non-MCP data to an MCP server's stdout or represented as incremental
CallToolResult content.
Workbench and tests
Workbench applies shell and boundary replacement events to one invocation
view. Tests can assert both the exact event sequence and the final Agent
Document without treating a fixture as a real host receipt.
Error and safety semantics
- input validation finishes before route rendering starts;
- failures before the shell return no partial success;
- represented boundary failures may allow independent siblings to complete;
- the final result distinguishes success, represented errors, and invocation
failure;
- JSX does not grant mutation authority;
- the request
AbortSignal reaches child processes, file/network operations,
the Flight renderer, decoder, and projector;
- stale Flight payloads are rejected using artifact/protocol identity rather
than decoded against a different build manifest.
Delivery steps
- Introduce Agent Document node and event contracts beside current lowerers.
- Replace manual component invocation with React-owned RSC/Flight execution.
- Deliver final-only decoding first while retaining the live Flight boundary.
- Add Suspense shell/replacement and progress events.
- Add MCP, CLI, rendered-script, Workbench, and test projectors.
- Migrate Audiobook Curator routes.
- Remove the synchronous lowerers after compatibility coverage.
- Rename or redefine
@agent-bundle/rsc-runtime so the package claim matches
delivered behavior.
Acceptance criteria
- Async Server Components and Suspense are executed by React, not a recursive
element walker.
- A generated route preserves a live Flight stream through decoding with
cancellation and backpressure.
- The renderer exposes one versioned document/event contract and low-level
projectors that later generated CLI/script/Workbench features consume; its
own MCP and test projections preserve equivalent final meaning.
- Ordinary MCP calls receive optional progress notifications followed by one
final CallToolResult. Task-augmented calls receive a CreateTaskResult
first and retrieve that final result through tasks/result. Neither mode
encodes partial content in progress messages.
- TTY and NDJSON expose ordered live events, while piped Markdown contains no
obsolete fallback content.
- Rich content is emitted only when the selected target capability supports it
or an explicit fallback exists.
- A post-completion producer is rejected and receives a typed handoff-required
outcome that the later directed-notice feature can consume.
- Output budgets, boundary errors, stale artifact identity, cancellation, and
source-deleted packed artifacts have direct Rstest coverage.
Design references
Stack position
Full meta-framework stack
MCP-hosted render runtime and thin host clients
The RSC renderer is not limited to CLI or browser-style consumers. It is the
shared internal request protocol for generated MCP tools and semantic event
routes.
MCP tool execution
MCP tools/call
-> validate input
-> create typed tool-route props and request context
-> render the route through Flight
-> incrementally decode Agent Render events
-> project progress where MCP permits it
-> return one final CallToolResult
In this sense a generated MCP tool is a Flight render call internally. Flight
does not replace MCP JSON-RPC or become an undocumented partial-result format;
it replaces hand-wired application execution behind the MCP handler. The MCP
server process may own the RSC renderer, provider graph, caches, and optional
state service for its lifetime.
Hook and event execution
A generated hook wrapper is a thin client rather than a second application
runtime. It:
- reads the complete host hook envelope;
- validates and normalizes canonical identity while retaining the complete
validated native payload as bounded, provenance-bearing event props;
- sends a structured render request to the shared runtime;
- consumes the Flight stream with the same artifact/protocol revision;
- projects the final Agent Document and permitted progress into the exact
native hook output envelope.
The local connection may be a host-native MCP-tool hook when its semantics are
proven equivalent, or a compiler-owned Unix socket, loopback, or child-process
IPC transport. The choice belongs to the host adapter and generated runtime,
not application code. Query-string encoding is not the canonical contract.
The renderer must support a discriminated invocation model such as
tool | event | cli | script | workbench, with each kind exposing typed props
and one shared request-context/provider surface. Cancellation and the host's
deadline propagate through the client, Flight request, provider work, decoder,
and final projector.
The Flight client/runtime path needs real proof for:
- a generated MCP tool call;
- at least two host hook envelopes projected through one event route;
- a warm MCP-hosted runtime serving consecutive requests without reloading the
application on every shell hook;
- runtime restart and artifact-epoch mismatch;
- a missing runtime producing an honest fallback or unavailable result rather
than fabricated success.
Process-lifetime state is not durable state. A host may restart or multiply MCP
server processes; #98 defines explicit volatile and durable state semantics.
Summary
Replace the current synchronous React-element lowerer with an actual React
Server Components execution path. Agent routes render a transport-neutral Agent
Document rather than HTML. One live internal Flight stream carries async Server
Component and Suspense work to an Agent Document decoder; MCP, CLI, scripts,
Workbench, and tests then project the same document according to their real
capabilities.
This issue owns rendering and projection. Typed request context comes from the
request-context feature, cross-request state comes from the optional state
kernel, and messages for future or different invocations come from the directed
notices feature.
Problem
@agent-bundle/rsc-runtimecurrently provides a smallMcp.*/Hook.*JSXvocabulary and synchronous lowerers. The lowerers call function components as
ordinary functions, walk the resulting elements, and produce one completed
protocol object. CLI output bypasses JSX and serializes the validated operation
result directly.
That is a useful React-shaped protocol DSL, but it is not a React renderer or
an RSC/Flight runtime. React does not own component execution, so async Server
Components, Suspense, request cancellation, boundary errors, backpressure, and
incremental reveal are unavailable. This is the implementation gap described
by #88.
Goals
manually invoking component functions.
integration APIs are not semver-stable across React minors.
Non-goals
context feature.
useSyncExternalStoreas server-sidepersistence.
progress notifications and one final result.
The compiler must preserve client/server boundary information, but action
transport is a separate feature.
Rendering architecture
The existing
examples/rsc-agent-runtimealready proves the importantimplementation seam: a worker installs request context and writes Flight. Its
caller currently buffers the complete Flight payload before decoding. The
framework implementation keeps this boundary but preserves the stream,
cancellation, and backpressure end to end.
Every renderer invocation is one logical invocation and produces one final
Agent Document. An ordinary MCP
tools/callmaps to one request and one finalCallToolResult. A task-augmented invocation spanstools/call,tasks/get,and
tasks/resultRPCs but resolves to the same final document. A later hostevent, state revision, or notice starts a new render; it does not write to a
closed Flight or hook response.
Authoring model
Route modules are async Server Component entrypoints. Data loading can begin
before or during rendering, and React owns suspension and resumption.
Server Components are server-side by convention and do not use a
'use server'directive.'use server'identifies callable Server Functions, notServer Components.
Async Server Components use the React-independent request accessor from the
context issue:
This renderer adds a narrow synchronous bridge:
useAgent()unwraps one stable request-scoped promise with Reactuse. It isnot a subscription or state container. Application hooks such as
useWorktree()may delegate to it in synchronous Server Components, whileasync components should prefer
await agent()or an async provider accessor.Agent Document vocabulary
The initial public vocabulary is deliberately protocol-oriented rather than a
browser DOM:
The committed document is a versioned, finite, immutable, serializable value:
Nodes contain no functions, class instances, streams, request-context handles,
or host-native objects. Projectors may impose stricter roots—for example, MCP
structured content is object-valued—so an array/scalar
valuemust be wrapped,rendered as text, or rejected according to an explicit route projection.
Unsupported HTML elements fail clearly. Image, audio, resource, and other rich
content are capability-gated per projector. For example, MCP permits audio
content blocks, but a selected host/model may not consume them. The projector
must use a declared fallback or fail; it never silently drops content or
pretends that transcription occurred.
Agent.Markdownremains a semantic text leaf. A browser-facing projector mayuse TanStack Markdown or another audited parser to render that leaf. The core
renderer does not parse Markdown into React only to turn it back into an Agent
Document.
Agent.Progressrepresents progress in the current document snapshot or aSuspense fallback; JSX does not create a mutable progress channel by itself.
Repeated progress events come from the request-scoped progress reporter or a
framework-owned streaming producer and follow the render-event contract below.
Render event protocol
Rules:
completecontains the canonical final document;replacemeans a suspended subtree becameready;
complete;project file/network/command work receives cancellation and only receives
backpressure when its own capability explicitly supports it.
Projection contracts
MCP
The standards-compatible MCP projector:
notifications/progressonly when the caller supplied a progresstoken;
short message into that notification;
CallToolResultwith supported content blocks andoptional object-valued structured content.
For an ordinary
tools/call, progress is out-of-band and the request resolvesto that final result. For a task-augmented call, the client explicitly opts in,
the server and tool advertise task support, the initial request returns a
CreateTaskResult, and the client retrieves status and the final result throughthe Tasks operations. The projector preserves the caller's opaque progress
token; progress remains optional and is emitted only while the ordinary request
or task is active. The token remains valid through a task's terminal state.
Tasks are optional lifecycle and deferred-result retrieval, not a rich
partial-result stream.
For ordinary MCP requests,
notifications/cancelledmaps into the rendererAbortSignal. Task-augmented requests usetasks/cancel. Under MCP2025-11-25, an HTTP SSE disconnect by itself is not cancellation.A future Agent Bundle MCP extension may negotiate shell/patch delivery, but the
portable projector does not assume it.
CLI and scripts
enter the renderer.
NDJSON render events are an Agent Bundle CLI/script dialect. They are never
written as non-MCP data to an MCP server's stdout or represented as incremental
CallToolResultcontent.Workbench and tests
Workbench applies shell and boundary replacement events to one invocation
view. Tests can assert both the exact event sequence and the final Agent
Document without treating a fixture as a real host receipt.
Error and safety semantics
failure;
AbortSignalreaches child processes, file/network operations,the Flight renderer, decoder, and projector;
than decoded against a different build manifest.
Delivery steps
@agent-bundle/rsc-runtimeso the package claim matchesdelivered behavior.
Acceptance criteria
element walker.
cancellation and backpressure.
projectors that later generated CLI/script/Workbench features consume; its
own MCP and test projections preserve equivalent final meaning.
final
CallToolResult. Task-augmented calls receive aCreateTaskResultfirst and retrieve that final result through
tasks/result. Neither modeencodes partial content in progress messages.
obsolete fallback content.
or an explicit fallback exists.
outcome that the later directed-notice feature can consume.
source-deleted packed artifacts have direct Rstest coverage.
Design references
useand Suspense integrationStack position
Full meta-framework stack
MCP-hosted render runtime and thin host clients
The RSC renderer is not limited to CLI or browser-style consumers. It is the
shared internal request protocol for generated MCP tools and semantic event
routes.
MCP tool execution
In this sense a generated MCP tool is a Flight render call internally. Flight
does not replace MCP JSON-RPC or become an undocumented partial-result format;
it replaces hand-wired application execution behind the MCP handler. The MCP
server process may own the RSC renderer, provider graph, caches, and optional
state service for its lifetime.
Hook and event execution
A generated hook wrapper is a thin client rather than a second application
runtime. It:
validated native payload as bounded, provenance-bearing event props;
native hook output envelope.
The local connection may be a host-native MCP-tool hook when its semantics are
proven equivalent, or a compiler-owned Unix socket, loopback, or child-process
IPC transport. The choice belongs to the host adapter and generated runtime,
not application code. Query-string encoding is not the canonical contract.
The renderer must support a discriminated invocation model such as
tool | event | cli | script | workbench, with each kind exposing typed propsand one shared request-context/provider surface. Cancellation and the host's
deadline propagate through the client, Flight request, provider work, decoder,
and final projector.
The Flight client/runtime path needs real proof for:
application on every shell hook;
than fabricated success.
Process-lifetime state is not durable state. A host may restart or multiply MCP
server processes; #98 defines explicit volatile and durable state semantics.