From 1b05d6031e0e13715e2f77e829caf842692733f3 Mon Sep 17 00:00:00 2001 From: Michael Lam Date: Tue, 26 May 2026 04:05:22 -0700 Subject: [PATCH] docs(runtime): define runner client backend gate --- CHANGELOG.md | 4 ++ docs/rfcs/hermes-run-adapter-contract.md | 65 ++++++++++++++++++++++++ tests/test_runtime_adapter_seam.py | 17 +++++++ 3 files changed, 86 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5af702c4..645e167d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ## [Unreleased] +### Added + +- **PR #2972** by @Michaelyklam (refs #1925) — Advance the runtime-adapter RFC after the Slice 4e route-selection harness shipped in v0.51.129. The RFC now defines the Slice 4f supervised local runner client backend gate: replace the bounded `runner-local` 501 only when explicitly configured, prove restart/reattach from durable runner/journal state, preserve the public chat-start field whitelist, require cancel as the first live runner-owned control, and keep active-run discovery/supervision out of new WebUI process-local runtime-surrogate globals. + ## [v0.51.137] — 2026-05-25 — Release DI (stage-batch19 — 6-PR medium-risk batch) ### Added diff --git a/docs/rfcs/hermes-run-adapter-contract.md b/docs/rfcs/hermes-run-adapter-contract.md index 71ef4da2..d66a9a4f 100644 --- a/docs/rfcs/hermes-run-adapter-contract.md +++ b/docs/rfcs/hermes-run-adapter-contract.md @@ -905,6 +905,12 @@ Non-goals for Slice 4d: #### Slice 4e: Default-off runner chat-start route-selection harness +Status as of 2026-05-24: shipped in v0.51.129 via #2794. The route-selection +harness now makes adapter mode selection explicit: `legacy-direct` remains the +default, `legacy-journal` still delegates to the existing journaled legacy path, +and `runner-local` returns a bounded not-configured response instead of silently +starting an in-process legacy run. + The first implementation after the Slice 4d gate should wire the `/api/chat/start` selection point to the existing `RuntimeAdapter` factory without adding a supervised runner process yet. The harness must make the @@ -948,6 +954,65 @@ Non-goals for Slice 4e: - no removal of `legacy-direct` or `legacy-journal`; - no server-side queue endpoint or queue scheduler just for adapter symmetry. +#### Slice 4f: Supervised local runner client backend gate + +After the route-selection harness ships, the next reviewable step is not to make +`runner-local` the default. It is to define the first concrete supervised/local +runner client backend that can replace the bounded 501 path under the existing +feature flag and prove execution ownership has moved out of the main WebUI +request process. + +This slice is a contract gate before backend code lands. The goal is to pin the +minimum runner client behavior so the implementation cannot become a renamed +`STREAMS` / `CANCEL_FLAGS` / cached `AIAgent` surrogate inside `api/routes.py`. + +Scope: + +- define the runner client process boundary and lifecycle: how `start_run` + spawns or hands off work, how the child is supervised, and how terminal state + is recorded without a main-process active-run dictionary; +- require a durable runner-owned run id plus session-to-run lookup that a freshly + restarted WebUI process can discover without consulting old `STREAMS` entries; +- require ordered event replay through the existing journal/cursor surface, so + token, reasoning, progress, tool, usage, error, and done events render through + the same browser path as legacy replay; +- define cancel as the first required live control for active runner-owned runs, + with approval, clarify, goal, and queue either mapped to explicit runner + capabilities or returned as bounded unsupported/conflict `ControlResult` + values; +- keep profile, workspace, attachments, provider/model, toolset, source, and + metadata as explicit payload fields at the runner boundary rather than + depending on process-global WebUI environment mutation. + +Acceptance tests for Slice 4f: + +1. **501 path replaced only when configured.** Unset adapter mode and + `legacy-journal` behavior stay unchanged; `runner-local` uses the supervised + runner client only when the backend is explicitly configured. +2. **Restart/reattach proves ownership moved.** Start a runner-owned run, + discard/restart the WebUI server process, rediscover the active or terminal + run from durable runner/journal state, replay from cursor without duplicates, + and preserve cancel if the run is still active. +3. **No runtime-surrogate globals.** The main WebUI server does not gain new + module-level maps for runner-owned streams, cancel flags, approval/clarify + callbacks, cached agents, goal state, queue schedulers, or child-process run + registries. Supervision state belongs to the runner client/backend boundary. +4. **Stable browser contracts.** Successful chat-start responses remain limited + to the legacy-compatible field whitelist unless a later contract revision + explicitly exposes `run_id`, `status`, or `active_controls`. +5. **Bounded control gaps.** Unsupported runner controls return safe + `unsupported`, `not-active`, or `conflict` results; they must not fall back to + legacy callback queues for a runner-owned run. + +Non-goals for Slice 4f: + +- no default-on runner mode; +- no removal of the legacy in-process backends; +- no broad WebUI product-surface migration; +- no server-side queue scheduler just for adapter symmetry; +- no permanent WebUI-owned active-run discovery cache that duplicates runner or + future Hermes Runtime API responsibility. + ## 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 99de8d98..7579e31e 100644 --- a/tests/test_runtime_adapter_seam.py +++ b/tests/test_runtime_adapter_seam.py @@ -558,6 +558,7 @@ def test_rfc_defines_slice4e_runner_chat_start_route_selection_harness(): rfc = (routes.Path(__file__).parent.parent / "docs" / "rfcs" / "hermes-run-adapter-contract.md").read_text(encoding="utf-8") assert "#### Slice 4e: Default-off runner chat-start route-selection harness" in rfc + assert "Status as of 2026-05-24: shipped in v0.51.129 via #2794" in rfc assert "route `/api/chat/start` through `build_runtime_adapter(...)`" in rfc assert "`legacy-direct` stays default" in rfc assert "`legacy-journal`\ncontinues to delegate to the legacy in-process stream path" in rfc @@ -566,6 +567,22 @@ def test_rfc_defines_slice4e_runner_chat_start_route_selection_harness(): assert "`run_id`, `status`, and\n `active_controls` remain internal" in rfc assert "no supervised runner process yet" in rfc + +def test_rfc_defines_slice4f_supervised_local_runner_client_gate(): + 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 4f: Supervised local runner client backend gate" in rfc + assert "replace the bounded 501 path under the existing\nfeature flag" in rfc + assert "durable runner-owned run id plus session-to-run lookup" in rfc + assert "cancel as the first required live control" in rfc + assert "501 path replaced only when configured" in rfc + assert "Restart/reattach proves ownership moved" in rfc + assert "No runtime-surrogate globals" in rfc + assert "Successful chat-start responses remain limited\n to the legacy-compatible field whitelist" in rfc + assert "Unsupported runner controls return safe\n `unsupported`, `not-active`, or `conflict` results" in rfc + assert "no permanent WebUI-owned active-run discovery cache" in rfc + def test_runner_runtime_adapter_passes_explicit_start_payload_without_env_mutation(monkeypatch): runtime = importlib.import_module("api.runtime_adapter") captured = []