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 parent
6864739e55
commit
f563d37244
3 files changed
+104
-5
No files matched your search
+42
-5
@@ -1,11 +1,13 @@
|
|||||||
"""RuntimeAdapter seam for WebUI-owned run execution.
|
"""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``
|
legacy direct route; enabling ``HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal``
|
||||||
routes through this protocol-translator facade over the same legacy execution
|
routes through this protocol-translator facade over the same legacy execution
|
||||||
path plus the Slice 1 run journal. This module intentionally does not own
|
path plus the Slice 1 run journal. Slice 4 adds a default-off runner-local
|
||||||
AIAgent instances, cancellation flags, approval callbacks, clarify callbacks, or
|
selection point for tests and future runner backends, but live chat routes still
|
||||||
new long-lived queues.
|
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
|
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_ENV = "HERMES_WEBUI_RUNTIME_ADAPTER"
|
||||||
_RUNTIME_ADAPTER_DIRECT = "legacy-direct"
|
_RUNTIME_ADAPTER_DIRECT = "legacy-direct"
|
||||||
_RUNTIME_ADAPTER_JOURNAL = "legacy-journal"
|
_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)
|
@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
|
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:
|
def _cursor_to_after_seq(cursor: str | None) -> int | None:
|
||||||
if cursor in (None, ""):
|
if cursor in (None, ""):
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -780,6 +780,12 @@ client/backend and explicit route selection.
|
|||||||
|
|
||||||
#### Slice 4c: Feature-flagged runner backend and restart/reattach harness
|
#### 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
|
After the facade exists, the next narrow implementation slice should add a real
|
||||||
runner-client/backend selection point and a synthetic restart/reattach harness,
|
runner-client/backend selection point and a synthetic restart/reattach harness,
|
||||||
without routing normal browser chat to that backend yet.
|
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_enabled({}) is False
|
||||||
assert runtime.runtime_adapter_mode({"HERMES_WEBUI_RUNTIME_ADAPTER": "legacy-journal"}) == "legacy-journal"
|
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_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"
|
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():
|
def test_legacy_journal_adapter_start_run_delegates_without_owning_runtime_state():
|
||||||
runtime = importlib.import_module("api.runtime_adapter")
|
runtime = importlib.import_module("api.runtime_adapter")
|
||||||
calls = []
|
calls = []
|
||||||
|
|||||||
Reference in new issue
Block a user