Merge pull request #2972

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
nesquena-hermes
2026-05-26 23:50:51 +00:00
3 changed files with 86 additions and 0 deletions
+4
View File
@@ -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.
### Changed
- Contributor guidance now requires explicit `Contract Routing` for contract-affecting PRs and `Contract Change` when a PR intentionally changes an existing product, runtime, or review contract. Contract tests must move with the corresponding docs instead of silently redefining behavior by themselves, with the current static coverage documented as advisory rather than a GitHub policy gate.
+65
View File
@@ -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.
+17
View File
@@ -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 = []