Stage 389: PR #2627

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
nesquena-hermes
2026-05-20 16:41:45 +00:00
2 changed files with 79 additions and 5 deletions
+64 -5
View File
@@ -94,11 +94,16 @@ adapter-seam work:
`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.
- #2575 shipped the Slice 4a runner/sidecar contract gate in v0.51.93. The next
implementation step can add runner-backend adapter plumbing, but it must stay
default-off, keep legacy fallback intact, pass explicit profile/workspace/model
payloads instead of mutating WebUI process globals, and avoid recreating
`STREAMS` / `CANCEL_FLAGS` / approval queues / clarify queues under new names.
- #2575 shipped the Slice 4a runner/sidecar contract gate in v0.51.93.
- #2599 shipped the Slice 4b `RunnerRuntimeAdapter` facade in v0.51.94. The
facade normalizes an injected runner client's start / observe / status /
control responses without owning `AIAgent`, streams, cancellation flags,
approval queues, clarify queues, goal state, or cached-agent tables.
- The next implementation gate is a feature-flagged runner backend plus
restart/reattach harness. It must stay default-off, keep legacy fallback
intact, pass explicit profile/workspace/model payloads instead of mutating
WebUI process globals, and avoid recreating `STREAMS` / `CANCEL_FLAGS` /
approval queues / clarify queues under new names.
The next gate is runner-backend plumbing, not queue implementation
by default. Queue / continue routing should only move before Slice 4 if a future
@@ -753,6 +758,8 @@ Non-goals for Slice 4a:
#### Slice 4b: Runner adapter client facade
Status as of 2026-05-20: shipped in v0.51.94 via #2599.
The first code slice after the Slice 4a contract should be a small
`RunnerRuntimeAdapter` facade that delegates to an injected runner client. This
is still not the runner process itself. Its job is to pin the adapter-facing
@@ -771,6 +778,58 @@ normalization rules before route wiring or process supervision lands:
The implementation remains default-off until a later slice adds an actual runner
client/backend and explicit route selection.
#### Slice 4c: Feature-flagged runner backend and restart/reattach harness
After the facade exists, the next narrow implementation slice should add a real
runner-client/backend selection point and a synthetic restart/reattach harness,
without routing normal browser chat to that backend yet.
Scope:
- add a concrete runner-client factory behind an explicit mode such as
`HERMES_WEBUI_RUNTIME_ADAPTER=runner-local`, while preserving `legacy-direct`
and `legacy-journal` as the default/revert paths;
- validate that `StartRunRequest` carries explicit session, profile, workspace,
attachments, provider/model, toolset, source, and metadata fields into the
runner boundary without relying on WebUI process-global environment mutation;
- prove a recreated WebUI adapter can rediscover runner-owned status and replay
ordered events from the runner/journal surface after process-local state is
discarded;
- keep controls bounded through `ControlResult` values, with unsupported controls
returning `unsupported` / `not-active` rather than falling back to stale legacy
`STREAMS` or callback queues;
- keep the live `/api/chat/start` path on the legacy backend until the runner
backend has a passing restart/reattach harness and maintainer approval to wire
route selection.
Acceptance tests for Slice 4c:
1. **Default-off selection.** `legacy-direct` remains the default; `runner-local`
or any later runner mode is selected only by an explicit feature flag.
2. **No route-shape drift.** Adding the runner backend does not expand public
`/api/chat/start`, cancel, approval, clarify, goal, or status response shapes
while the route remains legacy-backed.
3. **Restart/reattach harness.** A fake or local runner fixture can start a run,
discard the first WebUI adapter instance, recreate the adapter, and still
observe ordered events plus terminal/live status from durable runner-owned
state.
4. **Control bounds.** Cancel / approval / clarify / queue / goal controls route
through the runner client only when the runner backend is selected, and
unsupported controls return bounded `ControlResult` values without consulting
legacy process-local state.
5. **No runtime-surrogate globals.** The main WebUI server must not gain new
module-level maps for runner-owned streams, cancellation flags, pending
approval/clarify callbacks, cached agents, or goal/queue schedulers.
Non-goals for Slice 4c:
- no default-on runner backend;
- no removal of the legacy in-process backend;
- no public response-shape expansion;
- no live chat route switch to the runner backend before the restart/reattach
harness is reviewed;
- no server-side queue endpoint or queue scheduler just for adapter symmetry.
## First Meaningful Success Criteria
The first meaningful milestones are deliberately split.
+15
View File
@@ -404,10 +404,25 @@ def test_rfc_defines_slice4_runner_contract_before_runner_code():
assert "no removal of the legacy in-process backend" in rfc
assert "no default-on runner mode" in rfc
assert "#### Slice 4b: Runner adapter client facade" in rfc
assert "Status as of 2026-05-20: shipped in v0.51.94 via #2599" in rfc
assert "delegates to an injected runner client" in rfc
assert "without relying on process-local `STREAMS`" in rfc
def test_rfc_defines_slice4c_runner_backend_harness_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 4c: Feature-flagged runner backend and restart/reattach harness" in rfc
assert "`HERMES_WEBUI_RUNTIME_ADAPTER=runner-local`" in rfc
assert "`legacy-direct` remains the default" in rfc
assert "No route-shape drift" in rfc
assert "Restart/reattach harness" in rfc
assert "discard the first WebUI adapter instance" in rfc
assert "No runtime-surrogate globals" in rfc
assert "no live chat route switch to the runner backend before the restart/reattach" in rfc
def test_runner_runtime_adapter_passes_explicit_start_payload_without_env_mutation(monkeypatch):
runtime = importlib.import_module("api.runtime_adapter")
captured = []