mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 19:20:16 +00:00
Stage 384: PR #2544
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
|
||||
### Documentation
|
||||
|
||||
- **PR #2544** by @Michaelyklam (refs #1925) — Implement the first Slice 3c RuntimeAdapter control routing. `RuntimeAdapter` / `LegacyJournalRuntimeAdapter` now expose `queue_message(...)` and `update_goal(...)` as protocol-translator delegates, and the `/api/goal` route uses `update_goal(...)` only when `HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal` is enabled while preserving the legacy-direct response shape. The change keeps `/queue`'s existing browser-side drain semantics and goal post-turn evaluation in the current agent loop; no runner/sidecar, WebUI-owned queue, goal scheduler, cached-agent table, or execution-survives-restart claim is introduced.
|
||||
- **PR #2511** by @franksong2702 (refs #2502 / #2503) — Update the `docs/ui-ux/` demo appearance controls to initialize as `class="dark" data-skin="slate"` instead of the deprecated `data-theme`-only buttons and legacy theme names. Brings the demo pages in line with the live Theme + Skin contract referenced from the new `docs/CONTRACTS.md` so contributors following the contract-index path don't land on stale demos.
|
||||
- **PR #2509** by @Michaelyklam (refs #1925) — Advance the runtime-adapter RFC after the Slice 3b approval/clarify seam shipped in v0.51.89. The RFC now marks Slice 3b as shipped and defines the next Slice 3c queue/continue + goal control gate: route those controls through `RuntimeAdapter.queue_message(...)` / `update_goal(...)` only after pinning stable response contracts, bounded unavailable-control behavior, replayable lifecycle/status evidence, ordering/idempotency expectations, and explicit non-goals for runner/sidecar ownership or a WebUI-owned queue/goal scheduler. Docs + adapter-seam regression test only — no runtime/control routing changes in this PR.
|
||||
## [v0.51.89] — 2026-05-18 — Release BM (stage-382 — 6-PR full sweep batch — runtime adapter approval/clarify seam + SOUL.md memory panel + #1855 resolve_model_provider fast-path + PWA sidebar spinner fix + /model active-provider preference + contributor contract docs index)
|
||||
|
||||
+36
-6
@@ -7798,6 +7798,18 @@ def _start_chat_stream_for_session(
|
||||
return response
|
||||
|
||||
|
||||
def _runtime_adapter_goal_action(goal_args: str) -> str:
|
||||
"""Return the bounded RuntimeAdapter goal action for WebUI /goal args."""
|
||||
action = str(goal_args or "").strip().lower()
|
||||
if not action or action == "status":
|
||||
return "status"
|
||||
if action in ("pause", "resume"):
|
||||
return action
|
||||
if action in ("clear", "stop", "done"):
|
||||
return "clear"
|
||||
return "set"
|
||||
|
||||
|
||||
def _handle_goal_command(handler, body):
|
||||
"""Handle WebUI /goal command controls and optional kickoff stream."""
|
||||
try:
|
||||
@@ -7870,12 +7882,30 @@ def _handle_goal_command(handler, body):
|
||||
)
|
||||
previous_goal_state = goal_state_snapshot(s.session_id, profile_home=profile_home)
|
||||
|
||||
payload = goal_command_payload(
|
||||
s.session_id,
|
||||
goal_args,
|
||||
stream_running=stream_running,
|
||||
profile_home=profile_home,
|
||||
)
|
||||
from api.runtime_adapter import LegacyJournalRuntimeAdapter, runtime_adapter_enabled
|
||||
|
||||
def _legacy_goal_update(session_id: str, _action: str, text: str) -> dict:
|
||||
return goal_command_payload(
|
||||
session_id,
|
||||
text,
|
||||
stream_running=stream_running,
|
||||
profile_home=profile_home,
|
||||
)
|
||||
|
||||
goal_adapter_action = _runtime_adapter_goal_action(goal_args)
|
||||
if runtime_adapter_enabled():
|
||||
adapter = LegacyJournalRuntimeAdapter(goal_delegate=_legacy_goal_update)
|
||||
control_result = adapter.update_goal(
|
||||
s.session_id,
|
||||
goal_adapter_action,
|
||||
goal_args,
|
||||
)
|
||||
# Slice 3c keeps the adapter as a structural seam only. Preserve the
|
||||
# public /api/goal response by passing through the legacy payload rather
|
||||
# than deriving HTTP behavior from ControlResult.accepted/status.
|
||||
payload = dict(control_result.payload)
|
||||
else:
|
||||
payload = _legacy_goal_update(s.session_id, goal_adapter_action, goal_args)
|
||||
if not payload.get("ok", True):
|
||||
status = 409 if payload.get("error") == "agent_running" else 400
|
||||
return j(handler, payload, status=status)
|
||||
|
||||
+45
-1
@@ -12,7 +12,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Iterable, Protocol
|
||||
from typing import Any, Callable, Iterable, Literal, Protocol
|
||||
|
||||
_RUNTIME_ADAPTER_ENV = "HERMES_WEBUI_RUNTIME_ADAPTER"
|
||||
_RUNTIME_ADAPTER_DIRECT = "legacy-direct"
|
||||
@@ -72,6 +72,7 @@ class ControlResult:
|
||||
status: str = "accepted"
|
||||
event_id: str | None = None
|
||||
safe_message: str | None = None
|
||||
payload: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class RuntimeAdapter(Protocol):
|
||||
@@ -81,6 +82,13 @@ class RuntimeAdapter(Protocol):
|
||||
def cancel_run(self, run_id: str) -> ControlResult: ...
|
||||
def respond_approval(self, run_id: str, approval_id: str, choice: str) -> ControlResult: ...
|
||||
def respond_clarify(self, run_id: str, clarify_id: str, response: str) -> ControlResult: ...
|
||||
def queue_message(self, run_id: str, message: str, *, mode: str = "queue") -> ControlResult: ...
|
||||
def update_goal(
|
||||
self,
|
||||
session_id: str,
|
||||
action: Literal["set", "pause", "resume", "clear", "status", "edit"],
|
||||
text: str = "",
|
||||
) -> ControlResult: ...
|
||||
|
||||
|
||||
def runtime_adapter_mode(environ: dict[str, str] | None = None) -> str:
|
||||
@@ -107,6 +115,23 @@ def _cursor_to_after_seq(cursor: str | None) -> int | None:
|
||||
|
||||
|
||||
def _active_control_result(value: Any) -> ControlResult:
|
||||
"""Normalize legacy delegate responses without changing their payloads.
|
||||
|
||||
``status`` is an adapter-level summary used by current control tests and
|
||||
future runtime backends. For legacy goal payloads it may mirror the goal
|
||||
action (``set`` / ``pause`` / ``status``), while public route behavior keeps
|
||||
using the payload itself to preserve existing HTTP response shapes.
|
||||
"""
|
||||
if isinstance(value, ControlResult):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
accepted = bool(value.get("ok", True))
|
||||
return ControlResult(
|
||||
accepted=accepted,
|
||||
status=str(value.get("status") or value.get("action") or ("accepted" if accepted else "not-active")),
|
||||
safe_message=value.get("message") if not accepted else None,
|
||||
payload=dict(value),
|
||||
)
|
||||
accepted = bool(value)
|
||||
return ControlResult(
|
||||
accepted=accepted,
|
||||
@@ -129,6 +154,8 @@ class LegacyJournalRuntimeAdapter:
|
||||
cancel_delegate: Callable[[str], Any] | None = None,
|
||||
approval_delegate: Callable[[str, str, str], Any] | None = None,
|
||||
clarify_delegate: Callable[[str, str, str], Any] | None = None,
|
||||
queue_delegate: Callable[[str, str, str], Any] | None = None,
|
||||
goal_delegate: Callable[[str, str, str], Any] | None = None,
|
||||
live_stream_lookup: Callable[[str], bool] | None = None,
|
||||
session_dir: Path | None = None,
|
||||
):
|
||||
@@ -136,6 +163,8 @@ class LegacyJournalRuntimeAdapter:
|
||||
self._cancel_delegate = cancel_delegate
|
||||
self._approval_delegate = approval_delegate
|
||||
self._clarify_delegate = clarify_delegate
|
||||
self._queue_delegate = queue_delegate
|
||||
self._goal_delegate = goal_delegate
|
||||
self._live_stream_lookup = live_stream_lookup or (lambda _run_id: False)
|
||||
self._session_dir = Path(session_dir) if session_dir is not None else None
|
||||
|
||||
@@ -221,3 +250,18 @@ class LegacyJournalRuntimeAdapter:
|
||||
if self._clarify_delegate is None:
|
||||
return ControlResult(False, status="unsupported", safe_message="Clarify is delegated to the legacy path.")
|
||||
return _active_control_result(self._clarify_delegate(run_id, clarify_id, response))
|
||||
|
||||
def queue_message(self, run_id: str, message: str, *, mode: str = "queue") -> ControlResult:
|
||||
if self._queue_delegate is None:
|
||||
return ControlResult(False, status="unsupported", safe_message="Queue is delegated to the legacy path.")
|
||||
return _active_control_result(self._queue_delegate(run_id, message, mode))
|
||||
|
||||
def update_goal(
|
||||
self,
|
||||
session_id: str,
|
||||
action: Literal["set", "pause", "resume", "clear", "status", "edit"],
|
||||
text: str = "",
|
||||
) -> ControlResult:
|
||||
if self._goal_delegate is None:
|
||||
return ControlResult(False, status="unsupported", safe_message="Goal is delegated to the legacy path.")
|
||||
return _active_control_result(self._goal_delegate(session_id, action, text))
|
||||
|
||||
@@ -82,11 +82,14 @@ adapter-seam work:
|
||||
- #2479 shipped the first Slice 3a implementation in v0.51.86, routing Stop
|
||||
Generation through `RuntimeAdapter.cancel_run(...)` only when
|
||||
`HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal` is enabled.
|
||||
- #2487 shipped the Slice 3b approval/clarify gate, and #2496 shipped approval /
|
||||
clarify response routing through the adapter seam in v0.51.89.
|
||||
- #2509 shipped the Slice 3c queue/continue + goal gate in v0.51.90.
|
||||
|
||||
The next gate is not the runner/sidecar yet. It is the next control migration
|
||||
gate: approval and clarify must be specified together before implementation,
|
||||
because both are callback-backed, user-mediated pauses in the active agent loop
|
||||
and have the same state-lifetime/replay hazards.
|
||||
The next gate is not the runner/sidecar yet. It is the Slice 3c implementation:
|
||||
add the queue/goal adapter methods and route accepted legacy control paths
|
||||
through them without moving goal evaluation, continuation scheduling, or
|
||||
execution ownership out of the existing agent loop.
|
||||
|
||||
## Goals
|
||||
|
||||
@@ -354,7 +357,15 @@ class RuntimeAdapter:
|
||||
`queue_message` is named for the legacy `/api/session/queue` payload: it
|
||||
accepts follow-up chat text rather than arbitrary runtime input. The method name
|
||||
does not require the HTTP route to change; it documents the adapter-level control
|
||||
semantics that a later Slice 3c implementation should preserve.
|
||||
semantics that a later Slice 3c implementation should preserve. The method enters
|
||||
the protocol before route wiring so queue/continue can land as a separate, small
|
||||
control-routing follow-up instead of being coupled to goal routing.
|
||||
|
||||
For `update_goal`, the `action` argument is the bounded adapter capability label.
|
||||
During the legacy-journal slice, the legacy goal parser still receives the full
|
||||
`text` payload and remains authoritative for details such as the body of
|
||||
`set <goal text>`. Future slices must not route goal semantics from `action`
|
||||
alone; doing so would drop the goal body and change `/api/goal` behavior.
|
||||
|
||||
Required data classes / payload fields:
|
||||
|
||||
@@ -364,7 +375,7 @@ Required data classes / payload fields:
|
||||
| `RunStartResult` | `run_id`, `session_id`, `stream_id`, `status`, `started_at`, `cursor`, `active_controls` | `stream_id` may remain the legacy stream id during Slice 2. |
|
||||
| `RunStatus` | `run_id`, `session_id`, `status`, `last_event_id`, `terminal_state`, `active_controls`, `pending_approval_id`, `pending_clarify_id` | Backed by live legacy state plus journal/session metadata. |
|
||||
| `RunEventStream` | ordered events matching Artifact 1, resumable from cursor | Can be implemented by existing SSE + journal replay at first. |
|
||||
| `ControlResult` | `accepted`, `status`, `event_id`, `safe_message` | Controls may still call existing handlers in Slice 2. |
|
||||
| `ControlResult` | `accepted`, `status`, `event_id`, `safe_message`, optional internal `payload` | Controls may still call existing handlers in Slice 2. Public HTTP responses must not leak adapter-only fields unless a later RFC expands them. |
|
||||
|
||||
The interface is intentionally narrower than a runner. It does not own `AIAgent`,
|
||||
tool execution, callback queues, cancellation flags, approval callbacks, or
|
||||
@@ -438,13 +449,14 @@ execution-survives-WebUI-restart gate remains deferred to Slice 4.
|
||||
### Slice 3: Control migration
|
||||
|
||||
Status as of 2026-05-18: Slice 3a cancel routing shipped in v0.51.86 via #2479,
|
||||
and Slice 3b approval/clarify routing shipped in v0.51.89 via #2496 / #2507.
|
||||
Slice 3b approval/clarify routing shipped in v0.51.89 via #2496 / #2507, and
|
||||
the Slice 3c queue/continue + goal gate shipped in v0.51.90 via #2509.
|
||||
Cancel was the smallest control-plane migration because it already had one clear
|
||||
browser affordance, one active-run target, and an existing legacy handler to
|
||||
delegate to. Approval and clarify then proved the same protocol-translator shape
|
||||
for user-mediated callback controls. Queue/continue and goal remain intentionally
|
||||
held behind the next gate because they can change run lifecycle semantics rather
|
||||
than just resolve an already-pending control.
|
||||
for user-mediated callback controls. Queue/continue and goal are the final
|
||||
pre-runner control migration because they can change run lifecycle semantics
|
||||
rather than just resolve an already-pending control.
|
||||
|
||||
Scope:
|
||||
|
||||
|
||||
@@ -230,6 +230,144 @@ def test_goal_endpoint_sets_goal_and_starts_kickoff_stream(monkeypatch, tmp_path
|
||||
assert started[0]["model_provider"] == "openai-codex"
|
||||
|
||||
|
||||
def test_goal_endpoint_preserves_response_shape_under_runtime_adapter_flag(monkeypatch, tmp_path):
|
||||
"""The Slice 3c adapter path delegates /goal without adding adapter-only fields."""
|
||||
from api import goals as webui_goals
|
||||
from api import routes
|
||||
|
||||
class FakeState:
|
||||
goal = "ship the feature"
|
||||
status = "active"
|
||||
turns_used = 1
|
||||
max_turns = 20
|
||||
last_verdict = None
|
||||
last_reason = None
|
||||
paused_reason = None
|
||||
|
||||
class FakeGoalManager:
|
||||
def __init__(self, session_id, default_max_turns=20):
|
||||
self.state = FakeState()
|
||||
|
||||
class FakeSession:
|
||||
session_id = "sid-goal-route"
|
||||
profile = "default"
|
||||
workspace = str(tmp_path)
|
||||
model = "gpt-5.5"
|
||||
model_provider = "openai-codex"
|
||||
messages = []
|
||||
context_messages = []
|
||||
pending_user_message = None
|
||||
active_stream_id = None
|
||||
|
||||
monkeypatch.setenv("HERMES_WEBUI_RUNTIME_ADAPTER", "legacy-journal")
|
||||
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
|
||||
monkeypatch.setattr(routes, "get_session", lambda sid: FakeSession())
|
||||
monkeypatch.setattr(routes, "j", lambda handler, payload, status=200, **kwargs: {"status": status, "payload": payload})
|
||||
|
||||
result = routes._handle_goal_command(object(), {"session_id": "sid-goal-route", "args": "status"})
|
||||
|
||||
assert result["status"] == 200
|
||||
assert result["payload"]["action"] == "status"
|
||||
assert result["payload"]["message_key"] == "goal_status_active"
|
||||
assert "run_id" not in result["payload"]
|
||||
assert "active_controls" not in result["payload"]
|
||||
|
||||
|
||||
def test_goal_endpoint_adapter_keeps_full_set_text_and_legacy_payload_status(monkeypatch, tmp_path):
|
||||
"""The adapter action label must not replace legacy parsing of full goal text."""
|
||||
from api import goals as webui_goals
|
||||
from api import routes
|
||||
|
||||
set_calls = []
|
||||
|
||||
class FakeState:
|
||||
goal = ""
|
||||
status = "active"
|
||||
turns_used = 0
|
||||
max_turns = 20
|
||||
last_verdict = None
|
||||
last_reason = None
|
||||
paused_reason = None
|
||||
|
||||
class FakeGoalManager:
|
||||
def __init__(self, session_id, default_max_turns=20):
|
||||
self.state = FakeState()
|
||||
|
||||
def set(self, text):
|
||||
set_calls.append(text)
|
||||
self.state.goal = text
|
||||
return self.state
|
||||
|
||||
class FakeSession:
|
||||
session_id = "sid-goal-route"
|
||||
profile = "default"
|
||||
workspace = str(tmp_path)
|
||||
model = "gpt-5.5"
|
||||
model_provider = "openai-codex"
|
||||
messages = []
|
||||
context_messages = []
|
||||
pending_user_message = None
|
||||
active_stream_id = None
|
||||
|
||||
monkeypatch.setenv("HERMES_WEBUI_RUNTIME_ADAPTER", "legacy-journal")
|
||||
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
|
||||
monkeypatch.setattr(routes, "get_session", lambda sid: FakeSession())
|
||||
monkeypatch.setattr(routes, "resolve_trusted_workspace", lambda workspace: tmp_path)
|
||||
monkeypatch.setattr(
|
||||
routes,
|
||||
"_resolve_compatible_session_model_state",
|
||||
lambda model, provider: (model, provider, False),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
routes,
|
||||
"_start_chat_stream_for_session",
|
||||
lambda session, **kwargs: {"stream_id": "goal-stream", "session_id": session.session_id},
|
||||
)
|
||||
monkeypatch.setattr(routes, "j", lambda handler, payload, status=200, **kwargs: {"status": status, "payload": payload})
|
||||
|
||||
result = routes._handle_goal_command(object(), {"session_id": "sid-goal-route", "args": "set foo"})
|
||||
|
||||
assert result["status"] == 200
|
||||
assert result["payload"]["action"] == "set"
|
||||
assert result["payload"]["kickoff_prompt"] == "set foo"
|
||||
assert set_calls == ["set foo"]
|
||||
|
||||
|
||||
def test_goal_endpoint_adapter_error_payload_still_controls_http_status(monkeypatch, tmp_path):
|
||||
"""The /goal route preserves legacy error/status handling under the adapter flag."""
|
||||
from api import goals as webui_goals
|
||||
from api import routes
|
||||
|
||||
class FakeGoalManager:
|
||||
state = None
|
||||
|
||||
def __init__(self, session_id, default_max_turns=20):
|
||||
pass
|
||||
|
||||
class FakeSession:
|
||||
session_id = "sid-goal-route"
|
||||
profile = "default"
|
||||
workspace = str(tmp_path)
|
||||
model = "gpt-5.5"
|
||||
model_provider = "openai-codex"
|
||||
messages = []
|
||||
context_messages = []
|
||||
pending_user_message = None
|
||||
active_stream_id = "running-stream"
|
||||
|
||||
monkeypatch.setenv("HERMES_WEBUI_RUNTIME_ADAPTER", "legacy-journal")
|
||||
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
|
||||
monkeypatch.setattr(routes, "get_session", lambda sid: FakeSession())
|
||||
monkeypatch.setitem(routes.STREAMS, "running-stream", {"queue": object()})
|
||||
monkeypatch.setattr(routes, "j", lambda handler, payload, status=200, **kwargs: {"status": status, "payload": payload})
|
||||
|
||||
result = routes._handle_goal_command(object(), {"session_id": "sid-goal-route", "args": "ship it"})
|
||||
|
||||
assert result["status"] == 409
|
||||
assert result["payload"]["ok"] is False
|
||||
assert result["payload"]["error"] == "agent_running"
|
||||
|
||||
|
||||
def test_routes_register_goal_endpoint_and_kickoff_stream():
|
||||
assert 'if parsed.path == "/api/goal"' in ROUTES_PY
|
||||
assert "return _handle_goal_command(handler, body)" in ROUTES_PY
|
||||
|
||||
@@ -12,6 +12,8 @@ def test_runtime_adapter_interface_and_legacy_journal_methods_exist():
|
||||
"cancel_run",
|
||||
"respond_approval",
|
||||
"respond_clarify",
|
||||
"queue_message",
|
||||
"update_goal",
|
||||
)
|
||||
for name in required:
|
||||
assert hasattr(runtime.RuntimeAdapter, name)
|
||||
@@ -24,17 +26,6 @@ def test_runtime_adapter_interface_and_legacy_journal_methods_exist():
|
||||
assert runtime.runtime_adapter_mode({"HERMES_WEBUI_RUNTIME_ADAPTER": "sidecar"}) == "legacy-direct"
|
||||
|
||||
|
||||
def test_queue_goal_adapter_methods_remain_docs_only_until_slice3c_implementation():
|
||||
# REMOVAL REQUIRED: delete this test as part of Slice 3c implementation (refs #1925),
|
||||
# at the point queue_message / update_goal land on RuntimeAdapter / LegacyJournalRuntimeAdapter.
|
||||
# See docs/rfcs/hermes-run-adapter-contract.md Slice 3c acceptance + non-goals.
|
||||
runtime = importlib.import_module("api.runtime_adapter")
|
||||
|
||||
for name in ("queue_message", "queue_input", "update_goal"):
|
||||
assert not hasattr(runtime.RuntimeAdapter, name)
|
||||
assert not hasattr(runtime.LegacyJournalRuntimeAdapter, name)
|
||||
|
||||
|
||||
def test_legacy_journal_adapter_start_run_delegates_without_owning_runtime_state():
|
||||
runtime = importlib.import_module("api.runtime_adapter")
|
||||
calls = []
|
||||
@@ -120,6 +111,30 @@ def test_legacy_journal_adapter_controls_delegate_to_existing_handlers():
|
||||
]
|
||||
|
||||
|
||||
def test_legacy_journal_adapter_queue_and_goal_delegate_without_owning_runtime_state():
|
||||
runtime = importlib.import_module("api.runtime_adapter")
|
||||
calls = []
|
||||
adapter = runtime.LegacyJournalRuntimeAdapter(
|
||||
queue_delegate=lambda run_id, message, mode: calls.append(("queue", run_id, message, mode)) or True,
|
||||
goal_delegate=lambda session_id, action, text: calls.append(("goal", session_id, action, text)) or {
|
||||
"ok": True,
|
||||
"action": action,
|
||||
"message": "Goal updated.",
|
||||
},
|
||||
)
|
||||
|
||||
queued = adapter.queue_message("r1", "follow up", mode="queue")
|
||||
goal = adapter.update_goal("s1", "set", "finish the task")
|
||||
|
||||
assert queued.accepted is True
|
||||
assert goal.accepted is True
|
||||
assert goal.payload["action"] == "set"
|
||||
assert calls == [
|
||||
("queue", "r1", "follow up", "queue"),
|
||||
("goal", "s1", "set", "finish the task"),
|
||||
]
|
||||
|
||||
|
||||
def test_legacy_journal_adapter_cancel_returns_bounded_not_active_status():
|
||||
runtime = importlib.import_module("api.runtime_adapter")
|
||||
calls = []
|
||||
@@ -156,6 +171,29 @@ def test_legacy_journal_adapter_approval_and_clarify_return_bounded_not_active_s
|
||||
assert clarify.status == "not-active"
|
||||
|
||||
|
||||
def test_legacy_journal_adapter_queue_and_goal_return_bounded_statuses():
|
||||
runtime = importlib.import_module("api.runtime_adapter")
|
||||
adapter = runtime.LegacyJournalRuntimeAdapter(
|
||||
queue_delegate=lambda run_id, message, mode: False,
|
||||
goal_delegate=lambda session_id, action, text: {
|
||||
"ok": False,
|
||||
"action": action,
|
||||
"error": "agent_running",
|
||||
"message": "Agent is running.",
|
||||
},
|
||||
)
|
||||
|
||||
queued = adapter.queue_message("already-finished-run", "follow up")
|
||||
goal = adapter.update_goal("s1", "set", "new goal")
|
||||
|
||||
assert queued.accepted is False
|
||||
assert queued.status == "not-active"
|
||||
assert goal.accepted is False
|
||||
assert goal.status == "set"
|
||||
assert goal.safe_message == "Agent is running."
|
||||
assert goal.payload["error"] == "agent_running"
|
||||
|
||||
|
||||
def test_chat_cancel_route_uses_adapter_only_when_flag_enabled():
|
||||
routes = importlib.import_module("api.routes")
|
||||
src = (routes.Path(__file__).parent.parent / "api" / "routes.py").read_text(encoding="utf-8")
|
||||
@@ -191,6 +229,35 @@ def test_approval_and_clarify_routes_use_adapter_only_when_flag_enabled():
|
||||
assert "HERMES_WEBUI_RUNTIME_ADAPTER" not in clarify_body
|
||||
|
||||
|
||||
def test_goal_route_uses_adapter_only_when_flag_enabled():
|
||||
routes = importlib.import_module("api.routes")
|
||||
src = (routes.Path(__file__).parent.parent / "api" / "routes.py").read_text(encoding="utf-8")
|
||||
goal_idx = src.index("def _handle_goal_command")
|
||||
goal_body = src[goal_idx:src.index("def _handle_chat_start", goal_idx)]
|
||||
|
||||
assert "runtime_adapter_enabled()" in goal_body
|
||||
assert "LegacyJournalRuntimeAdapter(goal_delegate=_legacy_goal_update)" in goal_body
|
||||
assert "goal_adapter_action = _runtime_adapter_goal_action(goal_args)" in goal_body
|
||||
assert "adapter.update_goal(" in goal_body
|
||||
assert "goal_adapter_action," in goal_body
|
||||
assert "payload = dict(control_result.payload)" in goal_body
|
||||
assert "else:\n payload = _legacy_goal_update" in goal_body
|
||||
assert "HERMES_WEBUI_RUNTIME_ADAPTER" not in goal_body
|
||||
|
||||
|
||||
def test_goal_adapter_action_is_bounded_to_slice3c_actions():
|
||||
routes = importlib.import_module("api.routes")
|
||||
|
||||
assert routes._runtime_adapter_goal_action("") == "status"
|
||||
assert routes._runtime_adapter_goal_action("status") == "status"
|
||||
assert routes._runtime_adapter_goal_action("pause") == "pause"
|
||||
assert routes._runtime_adapter_goal_action("resume") == "resume"
|
||||
assert routes._runtime_adapter_goal_action("clear") == "clear"
|
||||
assert routes._runtime_adapter_goal_action("stop") == "clear"
|
||||
assert routes._runtime_adapter_goal_action("done") == "clear"
|
||||
assert routes._runtime_adapter_goal_action("ship #1925") == "set"
|
||||
|
||||
|
||||
def test_approval_respond_does_not_fallback_to_oldest_when_explicit_id_is_stale():
|
||||
routes = importlib.import_module("api.routes")
|
||||
src = (routes.Path(__file__).parent.parent / "api" / "routes.py").read_text(encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user