Stage 398: PR #2696 — feat(runtime): add runner-local adapter selection (RuntimeAdapter slice 4c, feature-flagged)
Co-authored-by: Michaelyklam <Michaelyklam@users.noreply.github.com>
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
"""RuntimeAdapter seam for WebUI-owned run execution.
|
||||
|
||||
This is the #1925 Slice 2 seam only. The default WebUI chat path remains the
|
||||
This is the #1925 RuntimeAdapter seam. The default WebUI chat path remains the
|
||||
legacy direct route; enabling ``HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal``
|
||||
routes through this protocol-translator facade over the same legacy execution
|
||||
path plus the Slice 1 run journal. This module intentionally does not own
|
||||
AIAgent instances, cancellation flags, approval callbacks, clarify callbacks, or
|
||||
new long-lived queues.
|
||||
path plus the Slice 1 run journal. Slice 4 adds a default-off runner-local
|
||||
selection point for tests and future runner backends, but live chat routes still
|
||||
stay on the legacy path until a separate route-wiring slice is reviewed. This
|
||||
module intentionally does not own AIAgent instances, cancellation flags,
|
||||
approval callbacks, clarify callbacks, or new long-lived queues.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -17,7 +19,12 @@ from typing import Any, Callable, Iterable, Literal, Protocol
|
||||
_RUNTIME_ADAPTER_ENV = "HERMES_WEBUI_RUNTIME_ADAPTER"
|
||||
_RUNTIME_ADAPTER_DIRECT = "legacy-direct"
|
||||
_RUNTIME_ADAPTER_JOURNAL = "legacy-journal"
|
||||
_VALID_RUNTIME_ADAPTER_MODES = {_RUNTIME_ADAPTER_DIRECT, _RUNTIME_ADAPTER_JOURNAL}
|
||||
_RUNTIME_ADAPTER_RUNNER_LOCAL = "runner-local"
|
||||
_VALID_RUNTIME_ADAPTER_MODES = {
|
||||
_RUNTIME_ADAPTER_DIRECT,
|
||||
_RUNTIME_ADAPTER_JOURNAL,
|
||||
_RUNTIME_ADAPTER_RUNNER_LOCAL,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -106,6 +113,36 @@ def runtime_adapter_enabled(environ: dict[str, str] | None = None) -> bool:
|
||||
return runtime_adapter_mode(environ) == _RUNTIME_ADAPTER_JOURNAL
|
||||
|
||||
|
||||
def runtime_adapter_runner_enabled(environ: dict[str, str] | None = None) -> bool:
|
||||
return runtime_adapter_mode(environ) == _RUNTIME_ADAPTER_RUNNER_LOCAL
|
||||
|
||||
|
||||
def build_runtime_adapter(
|
||||
*,
|
||||
environ: dict[str, str] | None = None,
|
||||
legacy_adapter_factory: Callable[[], RuntimeAdapter] | None = None,
|
||||
runner_client_factory: Callable[[], Any] | None = None,
|
||||
) -> RuntimeAdapter | None:
|
||||
"""Build the configured RuntimeAdapter without changing route behavior.
|
||||
|
||||
``None`` means the safe default ``legacy-direct`` path should keep using the
|
||||
existing direct route. ``legacy-journal`` is opt-in and delegates to the
|
||||
supplied legacy factory. ``runner-local`` is also opt-in and only constructs
|
||||
a ``RunnerRuntimeAdapter`` around an injected client; this function does not
|
||||
create process-global runner state or wire live chat to the runner backend.
|
||||
"""
|
||||
mode = runtime_adapter_mode(environ)
|
||||
if mode == _RUNTIME_ADAPTER_DIRECT:
|
||||
return None
|
||||
if mode == _RUNTIME_ADAPTER_JOURNAL:
|
||||
if legacy_adapter_factory is None:
|
||||
raise NotImplementedError("legacy-journal mode requires a legacy adapter factory")
|
||||
return legacy_adapter_factory()
|
||||
if runner_client_factory is None:
|
||||
raise NotImplementedError("runner-local mode requires a runner client factory")
|
||||
return RunnerRuntimeAdapter(client=runner_client_factory())
|
||||
|
||||
|
||||
def _cursor_to_after_seq(cursor: str | None) -> int | None:
|
||||
if cursor in (None, ""):
|
||||
return None
|
||||
|
||||
@@ -780,6 +780,12 @@ client/backend and explicit route selection.
|
||||
|
||||
#### Slice 4c: Feature-flagged runner backend and restart/reattach harness
|
||||
|
||||
Status as of PR #TBD: implementation proposed. The code adds a default-off
|
||||
`runner-local` adapter selection point plus factory wiring for an injected runner
|
||||
client, while keeping live browser chat routes on the legacy backend. The
|
||||
restart/reattach harness remains synthetic/fake-runner based until a later slice
|
||||
introduces a supervised runner process.
|
||||
|
||||
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.
|
||||
|
||||
@@ -26,9 +26,65 @@ def test_runtime_adapter_interface_and_legacy_journal_methods_exist():
|
||||
assert runtime.runtime_adapter_enabled({}) is False
|
||||
assert runtime.runtime_adapter_mode({"HERMES_WEBUI_RUNTIME_ADAPTER": "legacy-journal"}) == "legacy-journal"
|
||||
assert runtime.runtime_adapter_enabled({"HERMES_WEBUI_RUNTIME_ADAPTER": "legacy-journal"}) is True
|
||||
assert runtime.runtime_adapter_mode({"HERMES_WEBUI_RUNTIME_ADAPTER": "runner-local"}) == "runner-local"
|
||||
assert runtime.runtime_adapter_runner_enabled({"HERMES_WEBUI_RUNTIME_ADAPTER": "runner-local"}) is True
|
||||
assert runtime.runtime_adapter_mode({"HERMES_WEBUI_RUNTIME_ADAPTER": "sidecar"}) == "legacy-direct"
|
||||
|
||||
|
||||
def test_runtime_adapter_factory_selects_only_explicit_default_off_modes():
|
||||
runtime = importlib.import_module("api.runtime_adapter")
|
||||
calls = []
|
||||
|
||||
class FakeRunnerClient:
|
||||
pass
|
||||
|
||||
def legacy_factory():
|
||||
calls.append("legacy")
|
||||
return runtime.LegacyJournalRuntimeAdapter(start_run_delegate=lambda request: {"stream_id": "s"})
|
||||
|
||||
def runner_factory():
|
||||
calls.append("runner")
|
||||
return FakeRunnerClient()
|
||||
|
||||
assert runtime.build_runtime_adapter(environ={}) is None
|
||||
|
||||
legacy = runtime.build_runtime_adapter(
|
||||
environ={"HERMES_WEBUI_RUNTIME_ADAPTER": "legacy-journal"},
|
||||
legacy_adapter_factory=legacy_factory,
|
||||
runner_client_factory=runner_factory,
|
||||
)
|
||||
assert isinstance(legacy, runtime.LegacyJournalRuntimeAdapter)
|
||||
|
||||
runner = runtime.build_runtime_adapter(
|
||||
environ={"HERMES_WEBUI_RUNTIME_ADAPTER": "runner-local"},
|
||||
legacy_adapter_factory=legacy_factory,
|
||||
runner_client_factory=runner_factory,
|
||||
)
|
||||
assert isinstance(runner, runtime.RunnerRuntimeAdapter)
|
||||
assert calls == ["legacy", "runner"]
|
||||
|
||||
|
||||
def test_runner_local_factory_requires_injected_client_and_does_not_fallback_to_legacy():
|
||||
runtime = importlib.import_module("api.runtime_adapter")
|
||||
calls = []
|
||||
|
||||
def legacy_factory():
|
||||
calls.append("legacy")
|
||||
return runtime.LegacyJournalRuntimeAdapter(start_run_delegate=lambda request: {"stream_id": "s"})
|
||||
|
||||
try:
|
||||
runtime.build_runtime_adapter(
|
||||
environ={"HERMES_WEBUI_RUNTIME_ADAPTER": "runner-local"},
|
||||
legacy_adapter_factory=legacy_factory,
|
||||
)
|
||||
except NotImplementedError as exc:
|
||||
assert "runner client factory" in str(exc)
|
||||
else:
|
||||
raise AssertionError("runner-local must require an injected runner client factory")
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_legacy_journal_adapter_start_run_delegates_without_owning_runtime_state():
|
||||
runtime = importlib.import_module("api.runtime_adapter")
|
||||
calls = []
|
||||
|
||||
Reference in New Issue
Block a user