fix(runtime): clarify goal adapter seam semantics
This commit is contained in:
1 parent
b23fb6ccaa
commit
6a68bab114
5 files changed
+119
-3
No files matched your search
+6
-2
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user