mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-12 03:42:08 +00:00
fix(gateway): defer goal status notices until after response delivery
Route goal status notices through the platform adapter send API and register post-delivery callbacks so completed-goal notices appear after the final assistant response. Also cancel queued synthetic goal continuations on /goal pause and /goal clear while preserving normal queued user messages.
This commit is contained in:
parent
7d66d30d77
commit
03ddff8897
3 changed files with 300 additions and 36 deletions
147
tests/gateway/test_goal_status_notice.py
Normal file
147
tests/gateway/test_goal_status_notice.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.config import Platform
|
||||
from gateway.platforms.base import MessageEvent, MessageType
|
||||
from gateway.run import GatewayRunner
|
||||
from gateway.session import SessionSource
|
||||
from hermes_cli.goals import CONTINUATION_PROMPT_TEMPLATE
|
||||
|
||||
|
||||
class FakeAdapter:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
self.callbacks = {}
|
||||
self._active_sessions = {}
|
||||
|
||||
async def send(self, chat_id, content, reply_to=None, metadata=None):
|
||||
self.calls.append(
|
||||
{
|
||||
"chat_id": chat_id,
|
||||
"content": content,
|
||||
"reply_to": reply_to,
|
||||
"metadata": metadata,
|
||||
}
|
||||
)
|
||||
return SimpleNamespace(success=True)
|
||||
|
||||
def register_post_delivery_callback(self, session_key, callback, *, generation=None):
|
||||
self.callbacks[session_key] = (generation, callback)
|
||||
|
||||
|
||||
def _goal_continuation_event(source, goal="finish the task"):
|
||||
return MessageEvent(
|
||||
text=CONTINUATION_PROMPT_TEMPLATE.format(goal=goal),
|
||||
message_type=MessageType.TEXT,
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_goal_status_notice_uses_adapter_send_with_thread_metadata():
|
||||
"""Regression: /goal judge status must use BasePlatformAdapter.send().
|
||||
|
||||
The old implementation checked for a non-existent send_message() method,
|
||||
so the goal could be marked done in state_meta without the visible
|
||||
"✓ Goal achieved" status line being delivered to Discord/Telegram.
|
||||
"""
|
||||
runner = GatewayRunner.__new__(GatewayRunner)
|
||||
adapter = FakeAdapter()
|
||||
runner.adapters = {Platform.DISCORD: adapter}
|
||||
|
||||
source = SessionSource(
|
||||
platform=Platform.DISCORD,
|
||||
chat_id="parent-channel",
|
||||
thread_id="thread-123",
|
||||
)
|
||||
|
||||
await runner._send_goal_status_notice(source, "✓ Goal achieved: done")
|
||||
|
||||
assert adapter.calls == [
|
||||
{
|
||||
"chat_id": "parent-channel",
|
||||
"content": "✓ Goal achieved: done",
|
||||
"reply_to": None,
|
||||
"metadata": {"thread_id": "thread-123"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_goal_status_notice_defers_until_post_delivery_callback():
|
||||
"""Regression: goal status must appear after the agent's visible reply.
|
||||
|
||||
_post_turn_goal_continuation runs before BasePlatformAdapter sends the
|
||||
returned final response. It should therefore register a post-delivery
|
||||
callback, not send the judge status immediately.
|
||||
"""
|
||||
runner = GatewayRunner.__new__(GatewayRunner)
|
||||
adapter = FakeAdapter()
|
||||
runner.adapters = {Platform.DISCORD: adapter}
|
||||
runner.config = SimpleNamespace(group_sessions_per_user=True, thread_sessions_per_user=False)
|
||||
|
||||
source = SessionSource(
|
||||
platform=Platform.DISCORD,
|
||||
chat_id="parent-channel",
|
||||
thread_id="thread-123",
|
||||
user_id="user-1",
|
||||
)
|
||||
|
||||
await runner._defer_goal_status_notice_after_delivery(source, "✓ Goal achieved: done")
|
||||
|
||||
assert adapter.calls == []
|
||||
assert len(adapter.callbacks) == 1
|
||||
|
||||
_, callback = next(iter(adapter.callbacks.values()))
|
||||
result = callback()
|
||||
if hasattr(result, "__await__"):
|
||||
await result
|
||||
|
||||
assert adapter.calls == [
|
||||
{
|
||||
"chat_id": "parent-channel",
|
||||
"content": "✓ Goal achieved: done",
|
||||
"reply_to": None,
|
||||
"metadata": {"thread_id": "thread-123"},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_clear_goal_pending_continuations_removes_slot_and_overflow_only():
|
||||
"""Regression: /goal pause/clear must cancel queued self-continuations.
|
||||
|
||||
A user-issued /goal pause can arrive after the judge queued the next
|
||||
continuation but before that queued turn runs. The queued synthetic goal
|
||||
continuation should be removed without dropping normal user /queue items.
|
||||
"""
|
||||
runner = GatewayRunner.__new__(GatewayRunner)
|
||||
adapter = FakeAdapter()
|
||||
adapter._pending_messages = {}
|
||||
runner._queued_events = {}
|
||||
|
||||
source = SessionSource(
|
||||
platform=Platform.DISCORD,
|
||||
chat_id="parent-channel",
|
||||
thread_id="thread-123",
|
||||
)
|
||||
session_key = "discord:parent-channel:thread-123"
|
||||
normal_event = MessageEvent(
|
||||
text="normal queued user message",
|
||||
message_type=MessageType.TEXT,
|
||||
source=source,
|
||||
)
|
||||
|
||||
adapter._pending_messages[session_key] = _goal_continuation_event(source)
|
||||
runner._queued_events[session_key] = [
|
||||
normal_event,
|
||||
_goal_continuation_event(source, goal="second continuation"),
|
||||
]
|
||||
|
||||
removed = runner._clear_goal_pending_continuations(session_key, adapter)
|
||||
|
||||
assert removed == 2
|
||||
assert adapter._pending_messages.get(session_key) is None
|
||||
assert runner._queued_events[session_key] == [normal_event]
|
||||
Loading…
Add table
Add a link
Reference in a new issue