Skip to content

test(docs): pin and document the thrown-route-error projection per surface (#492) - #506

Merged
ScriptedAlchemy merged 2 commits into
mainfrom
fix/492-thrown-route-error-projection
Sep 4, 2026
Merged

test(docs): pin and document the thrown-route-error projection per surface (#492)#506
ScriptedAlchemy merged 2 commits into
mainfrom
fix/492-thrown-route-error-projection

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Fixes #492.

Decision

No error.tsx / loading.tsx / template.tsx convention, and no behavior change: the MCP projector keeps the SDK default for a root throw. This PR pins what each surface does today and documents it per surface.

Why the projector is not changed to wrap a root throw into the Agent.Error shape

  • The layout shell is a property of a document; a root throw has none. Giving it _meta would mean rendering the layout around a synthetic child — the layout-level error prop the issue reserves for "only if a consumer asks". Neither surveyed app asks.
  • A [code] text prefix in the projector would create a third error shape beside the SDK default (which every other handler failure — resultSchema rejection, McpProjectionError — already uses) and the represented one; it would still lack _meta/structuredContent, so it would not close the gap the issue describes.
  • Prompts/resources have no isError channel; a wrapped shape would only ever apply to tools.
  • The one case where the framework can keep the shell — a nested Suspense boundary rejecting — it already does: the reconciler folds the rejection into the streamed document as an error node with code boundary, so the wire result is exactly <Agent.Error code="boundary">'s: layout _meta, structuredContent, isError: true, [boundary] <message>. The projector's case 'error': return; is therefore a decision (the error is already in the document), now pinned.

Findings worth calling out

  • Hooks are fail-open on a throw, not fail-closed. The issue text says "a throw fails the hook closed". What the code does: the wrapper writes the message to stderr, nothing to stdout, exits 1; every supported host documents exit 1 as a non-blocking error (Cursor: fail-open unless failClosed; Claude/Codex: stderr shown, action continues). A thrown tool/before does not deny. Docs now say so explicitly and point at outcome: 'deny'.
  • A rejected boundary inside an event route is invisible to the host. The hook projection reads Agent.Context and the result value only, so the error node contributes nothing and the host sees a normal pass-through with the surviving context. Pinned.

Tests

  • packages/agent-bundle/fixtures/route-harness/src/mcp/harness/tools/fault.tsx (new fixture, mode: ok | throw | reject-boundary); added to every hard-coded harness route list (mcp-in-memory, packed-stdio-projection, cli-dispatch, test-harness-manifest) and to contract-matrix-fixtures.ts with mode: 'ok'.
  • tests/route-unit/thrown-route-error.test.ts (new): root throw → render-failed, no document, no events; boundary rejection → represented-error, error node { code: 'boundary' }, layout metadata kept, events shell → error(boundaryId) → complete; throwing event route → render-failed (nothing to project); event route with a rejected boundary → projectEventDocument yields only the surviving context.
  • tests/projection/mcp-in-memory.test.ts: root throw → exactly { content: [{ type: 'text', text: 'fault: route threw' }], isError: true }, no _meta/structuredContent, session still usable; boundary rejection → _meta kept, [boundary] … text, isError: true, structuredContent present.
  • tests/generated-route-server.test.ts (real generated stdio process): a layout that stamps _meta; tool throw → SDK default with no _meta; prompt throw and resource throw → client rejects with a JSON-RPC error carrying the message; server survives. New standalone tool/before event route that throws → exit 1, stderr has the message, stdout empty.
  • tests/hooks.test.ts: a config-declared handler that throws → { code: 1, stderr: 'handler exploded\n', stdout: '' }.
  • tests/projection/cli-dispatch-rendered.test.ts (projected harness fault command): root throw → stderr fault: route threw, exit 1, empty stdout; boundary rejection → stderr [boundary] fault: boundary rejected (the render error event), Markdown stdout fault: reject-boundary\n\n**[boundary]** fault: boundary rejected, and under --json the route's value alone with the boundary message absent from the JSON; exit 1 in both.
  • Shared-runtime hook failure text (Event route rendering failed., runtime-failed) was already pinned by tests/event-ipc.test.ts; the docs now cite it.

Ran: pnpm lint, pnpm typecheck, pnpm test:unit (3106), pnpm test:route-unit (52), pnpm test:projection (148), integration-pool generated-route-server + hooks (42), pnpm docs:site:build (parity OK).

Docs

  • docs/framework-mode.md: new "What happens when a route throws" section — one table, one row per surface (MCP tools; prompts/resources; rendered CLI/scripts; plain CLI/scripts; event routes; renderRoute), two columns (root throw vs rejected boundary), plus the rationale for not wrapping.
  • website/docs/{en,zh}/guide/authoring/mcp.mdx: same section; the layout paragraph's "MCP transport failure" wording corrected (a tool throw is an isError result, not a transport failure) and linked to the new section.
  • website/docs/{en,zh}/guide/authoring/hooks.mdx: a paragraph on thrown routes/handlers (exit 1, non-blocking, does not deny).
  • docs/entry-conventions.md: same wording correction.

Changeset

None — no publishable behavior changed. The only non-tests/** change under a publishable package is the route-harness test fixture (packages/agent-bundle/fixtures/**, not shipped), so this PR carries the skip-changeset label per .changeset/README.md.

Review status

Codex reviewed 1550137a9 and opened two threads; both are addressed in 60c496f37 (no PR comments are posted from this side by instruction — answers live here):

  • hooks.mdx:224 — "Distinguish shared event-route errors from standalone errors." Correct: events/ipc.ts answers every handler failure with the generic runtime-failed / Event route rendering failed., so only runtime: 'standalone' routes and config-declared handlers surface the thrown message on stderr. All four doc locations (framework-mode, en/zh mcp.mdx, en/zh hooks.mdx) now say so explicitly and cite event-ipc.test.ts as the pin. Behavior unchanged.
  • framework-mode.md:210 — "Document boundary errors outside the JSON value." Correct: the error render event goes to stderr as [boundary] message, Markdown output carries **[boundary]** message, and --json prints the route's value alone. The row (and its en/zh copies) is rewritten to say exactly that, and a new cli-dispatch-rendered.test.ts case pins all three outputs plus the root-throw path.

Rebased onto bf1d1eef4 (main) to pick up the docs-plugin fix from #507 that made the first "Docs site" run red. Latest head reviewed by Codex: 1550137a9. Unreviewed head at merge time (if no fresh review arrives): 60c496f37.

@ScriptedAlchemy ScriptedAlchemy added the skip-changeset PR changes a publishable package but ships no observable change; changeset not required label Sep 4, 2026
@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 60c496f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 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-04T02:19:01.821691Z 1550137 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 4, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@506
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@506
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@506

commit: 60c496f

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1550137a92

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +222 to +224
A route or handler that **throws** produces no document and therefore no decision: the generated
wrapper writes the message to stderr, nothing to stdout, and exits `1`, which every supported host
documents as a non-blocking error — the pending action proceeds as it would after a pass-through.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish shared event-route errors from standalone errors

When a conventional event route uses the default runtime: 'shared', its thrown message never reaches the wrapper: packages/agent-bundle/src/events/ipc.ts:272-279 replaces every handler failure with Event route rendering failed., which is what the wrapper writes to stderr. Only standalone routes and config-declared handlers expose the original message, and the new tests exercise only those cases, so this documentation gives users incorrect debugging expectations for the default route mode. Document the two modes separately or change the IPC behavior.

AGENTS.md reference: AGENTS.md:L78-L81

Useful? React with 👍 / 👎.

Comment thread docs/framework-mode.md Outdated
| --- | --- | --- |
| MCP `tools/call` | The MCP SDK's default tool error: `{ content: [{ type: 'text', text: <error.message> }], isError: true }`. No `_meta` (the layout never rendered), no `structuredContent`, no `[code]` prefix. The session stays usable. | A represented error: the reconciler folds the rejected boundary into the streamed document as an error node with code `boundary`, so the result is exactly what `<Agent.Error code="boundary">` would produce — layout `_meta` kept, `structuredContent` from the route's `Agent.Result value`, `isError: true`, and a `[boundary] <message>` text block after the content that had already rendered. |
| MCP `prompts/get`, `resources/read` | A JSON-RPC error response carrying the message; the client call rejects. These surfaces have no `isError` channel. | The same represented document; the generated server returns the route's `resultSchema`-parsed value, so the prompt or resource result is whatever the route's value said. |
| Rendered CLI command / rendered script | The message on stderr, exit 1. Nothing on stdout: no Markdown, no `--json` value. | The document prints as usual with `**[boundary]** message` beside the content that had rendered (or as the `--json` value); exit 1, because any non-`success` document status exits 1 regardless of the command's `exitCode` policy. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document boundary errors outside the JSON value

For a rendered command or script invoked with --json, runRenderedInvocation writes the boundary error event to stderr at packages/agent-bundle/src/cli-entry.ts:536-540, while stdout receives only JSON.stringify(complete.value) at lines 572-575. The boundary message is therefore not part of the --json value as this row claims; machine consumers must check the nonzero exit status and stderr, or use --ndjson for error events.

AGENTS.md reference: AGENTS.md:L78-L81

Useful? React with 👍 / 👎.

@ScriptedAlchemy
ScriptedAlchemy force-pushed the fix/492-thrown-route-error-projection branch from 1550137 to 60c496f Compare September 4, 2026 02:45
@ScriptedAlchemy
ScriptedAlchemy merged commit 60f75c1 into main Sep 4, 2026
13 of 14 checks passed
@ScriptedAlchemy
ScriptedAlchemy deleted the fix/492-thrown-route-error-projection branch September 4, 2026 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR changes a publishable package but ships no observable change; changeset not required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decide and pin the thrown-route-error projection per surface instead of adopting error.tsx/loading.tsx file conventions (xref row 8)

1 participant