feat(runtime): expose the terminal capability to routes and scripts as request.terminal (#511) - #534
Conversation
🦋 Changeset detectedLatest commit: c869e09 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b19e2d82b6
ℹ️ 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".
| '@agent-bundle/runtime': patch | ||
| --- | ||
|
|
||
| Expose the process's terminal capability to routes and scripts instead of each plugin probing `process.stdout` itself. `(await agent()).terminal` is a new `Observed<AgentTerminal>` axis beside `lineage` and `workspace`: `{ hostSurface: 'cli' | 'mcp' | 'hook' | 'script' | 'workbench', stdout, stderr, sharesTarget }`, where each stream reports `kind: 'tty' | 'pipe' | 'none'`, `color: 'none' | 'basic' | '256' | 'truecolor'`, and `columns`/`rows` when known. The routed-CLI executable (plain, rendered, and projected MCP commands) and rendered scripts probe their own process once, honouring `FORCE_COLOR`, `CLICOLOR_FORCE`, `NO_COLOR`, `CLICOLOR=0`, `TERM=dumb`, `COLORTERM`/`TERM` depth, and `COLUMNS`/`LINES` overrides, and select their `tty`/piped output mode from the same value; generated MCP servers, event routes (shared or standalone), and Workbench replays report `none` on both streams and never guess; a custom `runAgentRequest` host that mounts none observes `unavailable` (`not-provided`). The executable envelope now calls `main(argv, { terminal })` for plain `main`-exporting scripts and bins through the dependency-free `agent-bundle/terminal-capability` module (typed as `ExecutableMainContext` from `agent-bundle`); a one-parameter `main` is unaffected. `agent-bundle/cli-entry` accepts `terminal` on `runGeneratedCliEntry`/`runGeneratedRenderedScript` and hands it to `execute`, `render`, and `createSession`. The `agent-bundle/test` harness mounts a deterministic synthetic value from the `tty` knob of `invokeCli`/`runScript`, mounts `none` for MCP and event routes in `renderRoute` and the in-memory MCP level, and accepts `context.terminal` through the identity seam. `AGENT_REQUEST_STORE_VERSION` is now 4. Fixes #511 |
There was a problem hiding this comment.
Rewrite the changeset as a release summary
This entry is emitted into the release notes, but it includes internal implementation details such as AGENT_REQUEST_STORE_VERSION and harness/runtime wiring, and ends with Fixes #511 rather than the required (#511). Rewrite it as a concise, user-facing imperative summary naming the new public capability and ending with the parenthesized PR reference.
AGENTS.md reference: AGENTS.md:L102-L105
Useful? React with 👍 / 👎.
| const lineage = snapshotObserved(init.lineage ?? unavailable<AgentLineage>()); | ||
| const session = snapshotObserved(init.session ?? unavailable<AgentSessionIdentity>()); | ||
| const signal = init.signal ?? new AbortController().signal; | ||
| const terminal = snapshotObserved(init.terminal ?? unavailable<AgentTerminal>()); |
There was a problem hiding this comment.
Mount terminal data in the runtime compatibility adapters
When an operation calls agent() through the public runRscCli or createRscMcpServer adapters, neither adapter supplies terminal to its runAgentRequest initializer (checked src/cli.ts:37-52 and src/mcp-server.ts:42-56), so this fallback returns unavailable. This contradicts the new documentation promising a native CLI capability and a derived terminal-free MCP capability, leaving the feature absent from two documented production paths; both adapters should mount the appropriate value.
AGENTS.md reference: AGENTS.md:L78-L81
Useful? React with 👍 / 👎.
430705f to
3c4ea4f
Compare
3c4ea4f to
1029032
Compare
…s request.terminal (#511) `(await agent()).terminal` is a new `Observed<AgentTerminal>` axis: per-stream TTY-ness, color depth, and columns/rows plus `sharesTarget` and the projection (`hostSurface`). The routed CLI and rendered scripts probe their process once (FORCE_COLOR, CLICOLOR_FORCE, NO_COLOR, CLICOLOR, TERM, COLORTERM, COLUMNS, LINES) and pick their output mode from the same value; MCP servers, event routes, and Workbench replays report `none` and never guess. The executable envelope passes `{ terminal }` to plain `main` scripts and bins through the dependency-free `agent-bundle/terminal-capability` module.
…ain-script terminal probe
…pters; rewrite the changeset as a release summary (review)
1029032 to
eb5ce95
Compare
…, re-pin the worker source hash
Fixes #511.
What
Routes and scripts get a read-only terminal capability from the framework instead of each plugin probing
process.stdout.isTTY/columns/FORCE_COLORitself.(await agent()).terminalis a newObserved<AgentTerminal>axis besidelineageandworkspace:Per stream rather than flat because the filing consumer (cargo-hauler
src/client/exec.ts) decides stdout and stderr color independently and asks whether the two share a target (2>&1) before merging a child's channels.Per-surface values (honest, never guessed)
hostSurfacestdout/stderr<target>/bin/<name>.mjs) — plain, rendered, or projected MCP commandclinativesrc/scripts/<name>.tsx)scriptnativemainscript / bincli(bin) /script(artifact)main(argv, { terminal })(ExecutableMainContext)mcpnone/none,color: 'none',sharesTarget: falsederivedhooknone/nonederivedworkbenchnone/nonederivedrunAgentRequesthost that mounts noneunavailable(not-provided)Color precedence (matches the informal standards and the consumer's own
colorEnabled):FORCE_COLORdecides outright (0/falseoff;''/1/truebasic;2256;3truecolor),CLICOLOR_FORCEforces on for a pipe at theCOLORTERM/TERMdepth,NO_COLOR(non-empty) andCLICOLOR=0force off,TERM=dumbnone, else tty-at-advertised-depth / pipe-none.COLUMNS/LINESoverride size. The routed CLI selects itstty-vs-Markdown mode from the same value, so plugin output and framework output can never disagree.How
packages/rsc-runtime/src/agent-request.ts:AgentTerminal*types,terminalon the handle/init,AGENT_REQUEST_STORE_VERSION3 → 4.packages/agent-bundle/src/terminal-capability.ts(new, plain Node, no deps; rslib entry +agent-bundle/terminal-capabilityalias):detectProcessTerminal,terminalColor,sharesOutputTarget,noTerminal. No Effect runtime in artifacts; the first-party CLI is untouched (no eagerTerminalloads — compatible with perf(cli): load the Effect terminal runtime lazily to restore cold-start time #530).cli-entry.ts:resolveTerminalprobes once per invocation (explicitterminaloption wins; legacyisTtystill honoured as a stdout override), drives the output mode, and handsterminaltoexecute/render/createSession.entry-shell.ts: CLI bin mountsavailable(context.terminal, 'native'); rendered-route worker mountsmessage.terminal(forwarded, never probed); MCP Flight worker forwardsmessage.terminal ?? unavailable;mainenvelope callsmain(argv, { terminal: detectProcessTerminal(<cli|script>) }).mcp-server-runtime.tshost scopes mountnoTerminal('mcp')/noTerminal('hook')and forwardcontext.terminalto the worker; standalone hook shell mounts thehookliteral; Workbench replay mountsworkbench.invokeCli/runScriptttyknob → deterministic synthetic terminal (80×24 basic tty, or two color-free pipes);renderRoutemounts the route kind's artifact value; in-memory MCP forwards the server'snone;context.terminalinjectable; plain-script child envelope mirrors the generated one.AgentTerminal*andExecutableMainContextfromagent-bundle/agent-bundle/routes.Tests
terminal-capability.test.ts(color precedence, tty/pipe/closed-fd kinds,COLUMNS/LINES,sharesTarget),entry-shell.test.ts(envelope, bin/worker/Flight-worker templates, digest pin),cli-routes.test.ts(mode ↔ terminal agreement,--json, legacyisTty),rsc-runtime/tests/agent-request.test.ts.renderRouteMCP →none, event route →hook/none/none, injection.invokeCliplain (default pipe /tty), projected MCP commandharness context --yes --json(→cli,--jsondoes not change the capability), injection;runScriptrendered (script/pipe/pipe,script/tty/tty) and plainchecksum --terminal(real child process probe); in-memory MCP →none.packed-stdio-projection.test.ts, throughopenPackedMcpServer+ spawnedbin/route-harness.mjsandscripts/checksum.mjs): pipe detection,FORCE_COLOR=3+COLUMNS=120,CLICOLOR_FORCEvsNO_COLOR,LINES; packed MCPcontext→none; shared-runtime event route →hook/none/none.Ran locally:
pnpm typecheck,pnpm lint,pnpm test:unit(3255 passed),pnpm test:route-unit,pnpm test:projection,node scripts/run-packed-tests.mjs packages/agent-bundle/tests/packed-stdio-projection.test.ts,pnpm docs:site:build.Docs
docs/entry-conventions.md(new "Terminal capability (request.terminal)" section + envelope),docs/effect-conventions.md(Terminal/Stdio cross-reference: the route-facing capability is plain Node, notTerminal), website en+zh:guide/authoring/mcp.mdx(request-context section + per-surface table),package-entries.mdx(envelope),scripts-assets.mdx. One patch changeset foragent-bundleand@agent-bundle/runtime.Coordination
fix/468-plugin-root-context) addsrequest.pluginand also bumpsAGENT_REQUEST_STORE_VERSIONto 4; whichever lands second rebases and bumps to 5. Overlapping files:agent-request.ts,entry-shell.ts,mcp-server-runtime.ts,test/{cli,mcp,render}.ts, the route-harnesscontextfixture tests,mcp.mdxen/zh.fix/runtime-install-gaps) touchesentry-shell.ts,mcp-server-runtime.ts, the harness, and the same docs pages; textual overlap only.perf/cli-cold-start): no shared files; this PR adds nothing tocli.tsand no EffectTerminalloads.Review status
Codex reviewed
b19e2d8(two P1 threads); both are addressed in4df2dd8:Changeset as a release summary — rewritten: user-facing, imperative, names the export/option surface, no
AGENT_REQUEST_STORE_VERSIONor harness wiring, ends withFixes #511 (#534).runRscCli/createRscMcpServernever mountedterminal—createRscMcpServernow mounts themcp/nonevalue (derived);runRscCligainsRscCliOptions.terminaland mounts itnativewhen the caller passes one, otherwise the axis stays honestlyunavailable(not-provided) — the adapter owns no probe (the probe isagent-bundle's dependency-free module, which@agent-bundle/runtimecannot import; duplicating it into the runtime for a compatibility adapter was judged not worth a second implementation). Both adapters are pinned inrsc-runtime/tests/agent-request.test.tsand documented in the per-surface tables (docs/entry-conventions.md,mcp.mdxen/zh).Reviewed head:
b19e2d8(pre-rebase; the same change ise89f9e9after rebasing over fix(skills): serve agent-bundle/meta to rendered skills and compile their JSX without the consumer react runtime #527/fix(host-contracts): guard Claude's .claude.json by its user-scope mcpServers only in the native smoke #529/perf(cli): load the Effect terminal runtime lazily to restore cold-start time #530/docs(effect): record rc.112 finalizer, abortSignal, and language-service caveats #531/docs(evaluations): show a typed outcome grader (EvalGraderFunction / EvalGraderContext / EvalScriptOutcome) #535 — clean rebases, no conflicts).Unreviewed heads (current SHAs):
370944c(test-only: unpin the inherited color level in the plain-script probe test — CI runners force color),084596a(the two fixes above),3c4ea4f(zh docs row mirroring the en one). CI was fully green on the pre-rebase head4df2dd8; the first post-rebase run hit apackages/workbench/tests/mcp-app-real.e2e.test.tsPlaywrightwaitForRequesttimeout (unrelated to this change; main's own runs are red on the Node 26 leg for other reasons) and was superseded by the rebase onto docs(evaluations): show a typed outcome grader (EvalGraderFunction / EvalGraderContext / EvalScriptOutcome) #535. Auto-merge (squash) is armed for green.perf(cli): load the Effect terminal runtime lazily to restore cold-start time #530 (
perf/cli-cold-start) merged underneath: no shared files, no EffectTerminalloads added here.