Summary
A route that streams a Suspense fallback of <Agent.Progress completed={0} message="…" /> produces a shell document carrying a progress node (visible in renderRouteEvents, the Workbench, and --ndjson), but the MCP projector never turns that node into a notifications/progress notification: projectMcpEventStream only notifies on progress events (case 'progress' in the event loop), and a progress node inside a shell/replace document is not one.
So an MCP client that passed a progressToken receives nothing while the page is suspended, even though the framework's own documentation frames Agent.Progress as the streaming progress surface and the shell demonstrably streamed it. The only way to reach MCP progress today is (await agent()).progress.report(...), which means every streamed page has to state the same message twice (once in the fallback, once in the report) or route it through a shared constant.
Repro
Preview agent-bundle@43abb2db0, a generated tool route:
async function Page({ input, signal }) {
await new Promise((resolve) => setTimeout(resolve, 50));
return <Agent.Result value={{ ok: true }}><Agent.Text>done</Agent.Text></Agent.Result>;
}
export default async function Route({ input, signal }) {
return (
<Suspense fallback={<Agent.Progress completed={0} message="Reaching the seedbox" />}>
<Page input={input} signal={signal} />
</Suspense>
);
}
const rendered = await renderRouteEvents('tool:seedbox/check', { input: {} });
// rendered.events: shell (root contains {"kind":"progress","message":"Reaching the seedbox"}), replace, complete
const projected = await projectTargetCapabilities(
rendered,
createTargetCapabilityFixture({ audio: false, image: false, progress: true, resource: false, richContentFallback: 'text' }),
);
projected.progress // [] ← expected one notification with message "Reaching the seedbox", progress 0
Adding await (await agent()).progress.report({ completed: 0, message: 'Reaching the seedbox' }) at the top of Page makes projected.progress non-empty; the fallback node alone never does.
Expected
One of:
- The projector emits a
notifications/progress for each new progress node that appears in a streamed shell/replace document (deduplicated by the same monotonic completed rule it applies to events), so a Suspense fallback is enough; or
docs/framework-mode.md / the Agent.Progress reference states explicitly that a progress node is document content only and that MCP progress requires progress.report, and the example (audiobook-curator audit_library, which does both with the same string) calls out why.
movie-library ships the double form (src/progress.ts announce(message) + the fallback reading the same computed message) with a comment pointing here.
Summary
A route that streams a
Suspensefallback of<Agent.Progress completed={0} message="…" />produces a shell document carrying aprogressnode (visible inrenderRouteEvents, the Workbench, and--ndjson), but the MCP projector never turns that node into anotifications/progressnotification:projectMcpEventStreamonly notifies onprogressevents (case 'progress'in the event loop), and a progress node inside ashell/replacedocument is not one.So an MCP client that passed a
progressTokenreceives nothing while the page is suspended, even though the framework's own documentation framesAgent.Progressas the streaming progress surface and the shell demonstrably streamed it. The only way to reach MCP progress today is(await agent()).progress.report(...), which means every streamed page has to state the same message twice (once in the fallback, once in the report) or route it through a shared constant.Repro
Preview
agent-bundle@43abb2db0, a generated tool route:Adding
await (await agent()).progress.report({ completed: 0, message: 'Reaching the seedbox' })at the top ofPagemakesprojected.progressnon-empty; the fallback node alone never does.Expected
One of:
notifications/progressfor each newprogressnode that appears in a streamedshell/replacedocument (deduplicated by the same monotoniccompletedrule it applies to events), so a Suspense fallback is enough; ordocs/framework-mode.md/ theAgent.Progressreference states explicitly that a progress node is document content only and that MCP progress requiresprogress.report, and the example (audiobook-curatoraudit_library, which does both with the same string) calls out why.movie-library ships the double form (
src/progress.tsannounce(message)+ the fallback reading the same computed message) with a comment pointing here.