You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Cross-reference matrix row 8 / suggestion 8 asked whether agent-bundle should adopt Next-style error.tsx / loading.tsx / template.tsx file conventions beside src/layout.tsx and src/mcp/<server>/layout.tsx (#396). Assessed against the agent-host render target (MCP result, hook decision, CLI stdout) rather than the browser, the file conventions do not remove hand-rolled code in any real app, but the assessment surfaced one real gap the framework should decide explicitly: what a thrown (not represented) route error projects to on each surface, and whether the layout shell survives it.
Today the two error paths diverge:
Route outcome
MCP tools/call
MCP prompts/get / resources/read
Layout _meta / shell
Code
<Agent.Error code> (represented)
isError: true + [code] message text, structuredContent from Agent.Result value, _meta from the layout
same document projection
kept
author-chosen
route (or its Suspense child) throws
SDK default: { content: [{ type: 'text', text: error.message }], isError: true } — no _meta, no structuredContent, no [code]
JSON-RPC error
lost (no document exists)
none
The second row is not documented in docs/framework-mode.md ("What reaches the MCP wire" lists only Agent.Error), and no test pins it. A rejected nested Suspense boundary is a third case: the reconciler emits a render error event with boundaryId (packages/rsc-runtime/src/reconciler.tssettledBoundaryInputs), and the MCP projector discards it (packages/rsc-runtime/src/project-mcp.ts:255, case 'error': return;); what the final CallToolResult contains for that route is not covered by any test in packages/rsc-runtime/tests or packages/agent-bundle/tests.
Evidence
Surveyed at examples/* (main 2e59d6e), agent-plugins/cargo-conductor (3f70b60), and agent-plugins/movie-libraryfeat/react-app-shell (f37a286), looking for hand-rolled error handling or fallback rendering inside tool, event, CLI, and script routes.
Error handling — errors are data, never boundaries. No route in any of the three codebases does try { … } catch → <Agent.Error>:
movie-library: 46 tool routes render through one ReceiptDocument (src/components/receipt.tsx) that switches on receipt.isError (a field of the legacy handler's CallToolResult) to emit <Agent.Error code> vs <Agent.Text>. The error is a value in the result, not a thrown exception; src/cli/shelf.tsx does the same over a { kind: 'failed' } union.
cargo-conductor: ErrorState / UnavailableState (src/components/states.tsx) render a DaemonHealth discriminated union (unreachable, open-failed, …) that lib/daemon-health.ts produces by catching at the client layer. Routes never see a throw.
examples: the only try/catch in route files are host-test/src/mcp/host-test/tools/reset.tsx:33 (folds the failure into the result value's stateReason) and rsc-agent-runtime/src/events/tool/after.tsx:23 (writes a probe, then rethrows — relies on fail-closed).
An error.tsx boundary would therefore replace zero lines in these apps; both real apps deliberately keep failures inside the document so the layout shell (shellMetadata → CallToolResult._meta.movieLibrary; metadata.hauler) is preserved on every outcome. That preservation is exactly what a thrown error loses today.
Loading fallbacks — repetition exists, but the cause is #448, not a missing file. movie-library has 39 route files with the identical shape
exportdefaultasyncfunctionRoute({ input, signal }){constprogress=searchingMessage(String(input.query),[…]);return(<Suspensefallback={<Agent.Progresscompleted={0}message={progress}/>}><Pageinput={input}progress={progress}signal={signal}/></Suspense>);}// Page: await announce(progress); // src/progress.ts, "exists because of agent-bundle#448"
cargo-conductor has two equivalent stream wrappers (src/components/streaming.tsxAwaitStream, LogStream). The duplicated part is the announce() + progress prop plumbing that mirrors the fallback message into progress.report() so MCP clients see a notifications/progress — the gap #448 describes. The fallback messages are per-route and input-dependent (searchingMessage(input.query, sources)), so a per-server src/mcp/<server>/loading.tsx could not express them without a switch over routes, which is more code than the current per-route <Suspense>. Fixing #448 (project a progress node in a streamed shell/replace document as a progress notification) deletes src/progress.ts and the progress prop from 39 routes; a loading.tsx convention deletes nothing.
Where the framework already stands (packages/rsc-runtime/src, packages/agent-bundle/src):
documentToCallToolResult sets isError for any status !== 'success'; Agent.Error → [code] message text (project-mcp.ts).
registerGeneratedRoutes (mcp-server-runtime.ts) awaits renderGeneratedRoute; a rejection propagates to @modelcontextprotocol/server, whose tools/call path returns createToolError(message) and whose prompt/resource paths raise a JSON-RPC error.
Rendered CLI/scripts: an escaped throw is stderr + exit 1 (build/entry-shell.ts, test/script.ts docs); represented errors print **[code]** message.
Event routes: a throw fails the hook closed; hook projection (events/projection.ts) only ever sees a complete document.
Proposed shape
Not a file convention. Decide and document the thrown-error contract per surface, and give the layout a chance to see it, in this order of preference:
Document the current behavior in docs/framework-mode.md "What reaches the MCP wire" (new row: "route throws → tool error text with no _meta/structuredContent; prompt/resource → JSON-RPC error; CLI → stderr + exit 1; hook → fail closed") and in website/docs/{en,zh}/guide/authoring/mcp.mdx. State that represented errors are the supported path and that the layout shell only wraps documents.
Pin the rejected-Suspense-boundary outcome with a route-unit + mcp-in-memory test (a fixture route whose child boundary rejects), so the projector's case 'error': return; is a decision rather than an omission.
Only if a consumer asks for it: an optional AgentLayoutProps.error?: { code: string; message: string } (or a layout-level errorFallback export) that lets the layout render a thrown error as <Agent.Error> inside its shell so _meta survives — the layout, not a new error.tsx, is the boundary the feat(routes): conventional shared layout module for rendered routes (#312) #396 scopes already define. This must be measured against the two apps above, which currently need nothing.
Explicitly out of scope: template.tsx (Next's remount-per-navigation semantics have no agent-host meaning) and loading.tsx (superseded by #448).
Acceptance
docs/framework-mode.md and the en/zh MCP authoring pages state what a thrown route error projects to on MCP tools, prompts, resources, rendered CLI/scripts, and hooks, and that the layout shell does not wrap it.
A test proves the CallToolResult (content, isError, absence of _meta/structuredContent) for (a) a route whose default export throws and (b) a route whose nested Suspense boundary rejects, at route-unit and mcp-in-memory proof levels.
No error.tsx / loading.tsx / template.tsx discovery is added to routes/graph.ts unless a real app's diff shows the code it removes.
Problem
Cross-reference matrix row 8 / suggestion 8 asked whether agent-bundle should adopt Next-style
error.tsx/loading.tsx/template.tsxfile conventions besidesrc/layout.tsxandsrc/mcp/<server>/layout.tsx(#396). Assessed against the agent-host render target (MCP result, hook decision, CLI stdout) rather than the browser, the file conventions do not remove hand-rolled code in any real app, but the assessment surfaced one real gap the framework should decide explicitly: what a thrown (not represented) route error projects to on each surface, and whether the layout shell survives it.Today the two error paths diverge:
tools/callprompts/get/resources/read_meta/ shell<Agent.Error code>(represented)isError: true+[code] messagetext,structuredContentfromAgent.Result value,_metafrom the layout{ content: [{ type: 'text', text: error.message }], isError: true }— no_meta, nostructuredContent, no[code]The second row is not documented in
docs/framework-mode.md("What reaches the MCP wire" lists onlyAgent.Error), and no test pins it. A rejected nested Suspense boundary is a third case: the reconciler emits a rendererrorevent withboundaryId(packages/rsc-runtime/src/reconciler.tssettledBoundaryInputs), and the MCP projector discards it (packages/rsc-runtime/src/project-mcp.ts:255,case 'error': return;); what the finalCallToolResultcontains for that route is not covered by any test inpackages/rsc-runtime/testsorpackages/agent-bundle/tests.Evidence
Surveyed at
examples/*(main2e59d6e),agent-plugins/cargo-conductor(3f70b60), andagent-plugins/movie-libraryfeat/react-app-shell(f37a286), looking for hand-rolled error handling or fallback rendering inside tool, event, CLI, and script routes.Error handling — errors are data, never boundaries. No route in any of the three codebases does
try { … } catch → <Agent.Error>:ReceiptDocument(src/components/receipt.tsx) that switches onreceipt.isError(a field of the legacy handler'sCallToolResult) to emit<Agent.Error code>vs<Agent.Text>. The error is a value in the result, not a thrown exception;src/cli/shelf.tsxdoes the same over a{ kind: 'failed' }union.ErrorState/UnavailableState(src/components/states.tsx) render aDaemonHealthdiscriminated union (unreachable,open-failed, …) thatlib/daemon-health.tsproduces by catching at the client layer. Routes never see a throw.try/catchin route files arehost-test/src/mcp/host-test/tools/reset.tsx:33(folds the failure into the result value'sstateReason) andrsc-agent-runtime/src/events/tool/after.tsx:23(writes a probe, then rethrows — relies on fail-closed).An
error.tsxboundary would therefore replace zero lines in these apps; both real apps deliberately keep failures inside the document so the layout shell (shellMetadata→CallToolResult._meta.movieLibrary;metadata.hauler) is preserved on every outcome. That preservation is exactly what a thrown error loses today.Loading fallbacks — repetition exists, but the cause is #448, not a missing file. movie-library has 39 route files with the identical shape
cargo-conductor has two equivalent stream wrappers (
src/components/streaming.tsxAwaitStream,LogStream). The duplicated part is theannounce()+progressprop plumbing that mirrors the fallback message intoprogress.report()so MCP clients see anotifications/progress— the gap #448 describes. The fallback messages are per-route and input-dependent (searchingMessage(input.query, sources)), so a per-serversrc/mcp/<server>/loading.tsxcould not express them without a switch over routes, which is more code than the current per-route<Suspense>. Fixing #448 (project aprogressnode in a streamed shell/replace document as a progress notification) deletessrc/progress.tsand theprogressprop from 39 routes; aloading.tsxconvention deletes nothing.Where the framework already stands (
packages/rsc-runtime/src,packages/agent-bundle/src):documentToCallToolResultsetsisErrorfor anystatus !== 'success';Agent.Error→[code] messagetext (project-mcp.ts).registerGeneratedRoutes(mcp-server-runtime.ts) awaitsrenderGeneratedRoute; a rejection propagates to@modelcontextprotocol/server, whosetools/callpath returnscreateToolError(message)and whose prompt/resource paths raise a JSON-RPC error.build/entry-shell.ts,test/script.tsdocs); represented errors print**[code]** message.events/projection.ts) only ever sees a complete document.Proposed shape
Not a file convention. Decide and document the thrown-error contract per surface, and give the layout a chance to see it, in this order of preference:
docs/framework-mode.md"What reaches the MCP wire" (new row: "route throws → tool error text with no_meta/structuredContent; prompt/resource → JSON-RPC error; CLI → stderr + exit 1; hook → fail closed") and inwebsite/docs/{en,zh}/guide/authoring/mcp.mdx. State that represented errors are the supported path and that the layout shell only wraps documents.mcp-in-memorytest (a fixture route whose child boundary rejects), so the projector'scase 'error': return;is a decision rather than an omission.AgentLayoutProps.error?: { code: string; message: string }(or a layout-levelerrorFallbackexport) that lets the layout render a thrown error as<Agent.Error>inside its shell so_metasurvives — the layout, not a newerror.tsx, is the boundary the feat(routes): conventional shared layout module for rendered routes (#312) #396 scopes already define. This must be measured against the two apps above, which currently need nothing.Explicitly out of scope:
template.tsx(Next's remount-per-navigation semantics have no agent-host meaning) andloading.tsx(superseded by #448).Acceptance
docs/framework-mode.mdand the en/zh MCP authoring pages state what a thrown route error projects to on MCP tools, prompts, resources, rendered CLI/scripts, and hooks, and that the layout shell does not wrap it.CallToolResult(content,isError, absence of_meta/structuredContent) for (a) a route whose default export throws and (b) a route whose nested Suspense boundary rejects, atroute-unitandmcp-in-memoryproof levels.error.tsx/loading.tsx/template.tsxdiscovery is added toroutes/graph.tsunless a real app's diff shows the code it removes.