fix(runtime): clarify goal adapter seam semantics

This commit is contained in:
Michael Lam committed 2026-05-18 12:10:17 -07:00
1 parent b23fb6ccaa
commit 6a68bab114
5 files changed
+119 -3

No files matched your search

+6 -2
View File
@@ -7892,16 +7892,20 @@ def _handle_goal_command(handler, body):
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,
_runtime_adapter_goal_action(goal_args),
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, _runtime_adapter_goal_action(goal_args), goal_args)
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)
+7
View File
@@ -115,6 +115,13 @@ 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):
+9 -1
View File
@@ -357,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:
+95
View File
@@ -273,6 +273,101 @@ def test_goal_endpoint_preserves_response_shape_under_runtime_adapter_flag(monke
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
+2
View File
@@ -237,7 +237,9 @@ def test_goal_route_uses_adapter_only_when_flag_enabled():
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