From 960c95cfe30d9511131c067603598ddfce21125f Mon Sep 17 00:00:00 2001 From: Michael Lam Date: Mon, 18 May 2026 21:06:05 -0700 Subject: [PATCH] docs(runtime): define runner sidecar gate --- CHANGELOG.md | 3 + docs/rfcs/hermes-run-adapter-contract.md | 85 +++++++++++++++++++++--- tests/test_runtime_adapter_seam.py | 19 +++++- 3 files changed, 94 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 941c61eaa7d..3f9eac00ae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Documentation + +- **PR #TBD** by @Michaelyklam (refs #1925) — Advance the runtime-adapter RFC to the Slice 4 runner/sidecar planning gate after #2560 shipped the queue-staging clarification. The RFC now marks queue routing as staged by default, defines Slice 4a as a docs/test contract before any runner code lands, and pins default-off feature-flagging, restart/reattach success criteria, control parity, profile/workspace payload isolation, and explicit non-goals for legacy-backend removal or server-side queue scheduler work. ## [v0.51.92] — 2026-05-19 — Release BP (stage-385 — 7-PR full sweep batch — RFC Slice 3c clarification + workspace tree icon alignment + project move cache refresh + auto-compression handoff metadata + Grok OAuth provider catalog + anonymous custom endpoint picker fallback + PWA standalone reload + pull-to-refresh) diff --git a/docs/rfcs/hermes-run-adapter-contract.md b/docs/rfcs/hermes-run-adapter-contract.md index 2a7031569cf..34ad176ab84 100644 --- a/docs/rfcs/hermes-run-adapter-contract.md +++ b/docs/rfcs/hermes-run-adapter-contract.md @@ -52,7 +52,7 @@ The immediate goal is not to build a sidecar. The immediate goal is to define th browser contract, classify current runtime state, and gate the first reversible journal slice. -## Current Gate State — 2026-05-18 +## Current Gate State — 2026-05-19 Slice 1 is now past the first active validation gate: @@ -90,14 +90,17 @@ adapter-seam work: `HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal` is enabled, while preserving the legacy-direct response shape and leaving post-turn goal evaluation in the existing agent loop. - -The next gate is still not the runner/sidecar by default. Slice 3c's goal route -is shipped, and `queue_message(...)` remains a staged protocol method. Queue / -continue routing needs an explicit follow-up contract because the legacy `/queue` -path is browser-side queue/drain behavior today; no new server-side queue endpoint -or queue scheduler should be added just for adapter symmetry. If maintainers want -queue/continue to move before Slice 4, that follow-up should specify the exact -legacy entry point, response shape, and ordering/idempotency contract first. +- #2560 shipped the queue-staging clarification in v0.51.92. The RFC now treats + `queue_message(...)` as a staged protocol method only; `/queue` remains + browser-side queue/drain behavior, and no server-side queue endpoint or queue + scheduler should be added merely for adapter symmetry. + +The next gate is the runner/sidecar planning contract, not queue implementation +by default. Queue / continue routing should only move before Slice 4 if a future +maintainer decision identifies an existing server-side legacy entry point and +pins its response shape, ordering, and idempotency contract. Otherwise, keeping +`queue_message(...)` staged is the honest boundary while execution ownership +moves out of the main WebUI request process. ## Goals @@ -670,8 +673,19 @@ Non-goals for Slice 3c: ### Slice 4: Runner process / sidecar boundary -Explicitly deferred until Slice 1 has worked in production for at least one -release cycle and the adapter surface has review approval. +Slice 4 is the first gate that may move active execution ownership out of the +main WebUI request process. It should start as a docs/test contract PR before any +runner code lands. Slice 1's journal/replay layer has shipped and passed active +validation, Slice 2's default-off adapter seam has shipped, and Slice 3's +cancel/approval/clarify/goal control routing has proven the protocol-translator +pattern. Queue remains staged unless maintainers explicitly ask for a separate +pre-runner queue route. + +The Slice 4 implementation must not make the adapter a new runtime surrogate. +The runner boundary may own active execution, process supervision, run lifecycle, +and callback state, but those responsibilities must be centralized behind the +adapter/runner contract rather than recreated as scattered globals in the main +WebUI server. Scope: @@ -683,6 +697,55 @@ Scope: Revert path: disable runner backend and fall back to journaled legacy backend. +#### Slice 4a: Runner contract gate + +Before runner code lands, define a narrow contract that covers: + +1. **Backend selection and rollback.** The existing `legacy-direct` and + `legacy-journal` paths remain available. Any new runner backend is + feature-flagged, default-off, and revertible by switching the adapter mode back + to `legacy-journal` without deleting sessions or journal files. +2. **Process ownership.** The runner, not the main WebUI request process, owns + `AIAgent` construction/reuse, active run execution, cancellation flags, + approval/clarify callback wait state, and post-turn continuation evaluation + for runs assigned to that backend. +3. **Durable observation.** The main WebUI server observes through + `RuntimeAdapter.observe_run(...)`, `get_run(...)`, and the journal cursor. A + WebUI restart must not be required for the runner to finish writing ordered + events and terminal state. +4. **Restart/reattach success criterion.** Start a long-running run, restart only + `hermes-webui.service`, reload the session, rediscover the active or terminal + runner-owned run, replay/catch up from cursor without duplicate transcript / + tool / reasoning state, and preserve cancel if the run is still active. +5. **Control parity.** Cancel, approval, clarify, goal status/control, and any + accepted queue/continue behavior route through adapter methods with stable + browser response shapes. Unsupported controls return bounded `ControlResult` + states instead of silently falling back to stale in-process state. +6. **Profile/workspace isolation.** Runner startup receives explicit profile, + workspace, attachments, model/provider, toolset, and source metadata rather + than relying on process-global environment mutation in the WebUI server. + +Suggested contract tests before implementation: + +- source/RFC tests proving Slice 4 remains feature-flagged and default-off; +- a fake-runner adapter test that simulates WebUI restart by discarding server + process-local state while preserving runner/journal state, then verifies + `get_run` and replay recover the same terminal state; +- a control-parity fixture proving unsupported runner controls return bounded + `ControlResult` values and do not fall back to legacy `STREAMS` / + `CANCEL_FLAGS` state; +- a profile/workspace payload test proving runner requests carry explicit context + fields without mutating global `os.environ` in the main WebUI process. + +Non-goals for Slice 4a: + +- no removal of the legacy in-process backend; +- no default-on runner mode; +- no public chat-start/status response-shape expansion; +- no new server-side queue endpoint or scheduler just for adapter symmetry; +- no dependency on Hermes Agent shipping `/v1/runs` before WebUI can validate the + local runner boundary. + ## First Meaningful Success Criteria The first meaningful milestones are deliberately split. diff --git a/tests/test_runtime_adapter_seam.py b/tests/test_runtime_adapter_seam.py index ef1a795123b..1b6910c6e0f 100644 --- a/tests/test_runtime_adapter_seam.py +++ b/tests/test_runtime_adapter_seam.py @@ -309,7 +309,22 @@ def test_rfc_distinguishes_goal_routing_from_queue_route_staging(): rfc = (routes.Path(__file__).parent.parent / "docs" / "rfcs" / "hermes-run-adapter-contract.md").read_text(encoding="utf-8") assert "#2544 shipped the first Slice 3c implementation" in rfc + assert "#2560 shipped the queue-staging clarification" in rfc assert "route now uses `RuntimeAdapter.update_goal(...)`" in rfc - assert "`queue_message(...)` remains a staged protocol method" in rfc + assert "`queue_message(...)` as a staged protocol method only" in rfc assert "no new server-side queue endpoint" in rfc - assert "or queue scheduler should be added just for adapter symmetry" in rfc + assert "no server-side queue endpoint or queue\n scheduler should be added merely for adapter symmetry" in rfc + + +def test_rfc_defines_slice4_runner_contract_before_runner_code(): + routes = importlib.import_module("api.routes") + rfc = (routes.Path(__file__).parent.parent / "docs" / "rfcs" / "hermes-run-adapter-contract.md").read_text(encoding="utf-8") + + assert "#### Slice 4a: Runner contract gate" in rfc + assert "docs/test contract PR before any\nrunner code lands" in rfc + assert "feature-flagged, default-off" in rfc + assert "The runner, not the main WebUI request process, owns" in rfc + assert "restart only\n `hermes-webui.service`" in rfc + assert "profile,\n workspace, attachments, model/provider, toolset, and source metadata" in rfc + assert "no removal of the legacy in-process backend" in rfc + assert "no default-on runner mode" in rfc