mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 03:00:23 +00:00
fix: surface goal evaluation status
This commit is contained in:
@@ -413,6 +413,25 @@ def goal_command_payload(
|
||||
)
|
||||
|
||||
|
||||
def has_active_goal(
|
||||
session_id: str,
|
||||
*,
|
||||
profile_home: str | Path | None = None,
|
||||
) -> bool:
|
||||
"""Return True when the session has an active standing goal to evaluate."""
|
||||
sid = str(session_id or "").strip()
|
||||
if not sid:
|
||||
return False
|
||||
mgr = _manager(sid, profile_home=profile_home)
|
||||
if mgr is None:
|
||||
return False
|
||||
try:
|
||||
return bool(mgr.is_active())
|
||||
except Exception as exc:
|
||||
logger.debug("goal active-state check failed for session=%s: %s", sid, exc)
|
||||
return False
|
||||
|
||||
|
||||
def evaluate_goal_after_turn(
|
||||
session_id: str,
|
||||
last_response: str,
|
||||
|
||||
+32
-23
@@ -3106,35 +3106,44 @@ def _run_agent_streaming(
|
||||
# frontend surfaces the status line and queues continuation_prompt as
|
||||
# a normal next user message so /queue and user input keep priority.
|
||||
try:
|
||||
from api.goals import evaluate_goal_after_turn
|
||||
from api.goals import evaluate_goal_after_turn, has_active_goal
|
||||
|
||||
_last_goal_response = ''
|
||||
for _goal_msg in reversed(s.messages or []):
|
||||
if not isinstance(_goal_msg, dict) or _goal_msg.get('role') != 'assistant':
|
||||
continue
|
||||
_goal_content = _goal_msg.get('content', '')
|
||||
if isinstance(_goal_content, list):
|
||||
_goal_parts = []
|
||||
for _goal_part in _goal_content:
|
||||
if isinstance(_goal_part, dict):
|
||||
_goal_text = _goal_part.get('text') or _goal_part.get('content')
|
||||
if _goal_text:
|
||||
_goal_parts.append(str(_goal_text))
|
||||
_last_goal_response = '\n'.join(_goal_parts)
|
||||
else:
|
||||
_last_goal_response = str(_goal_content or '')
|
||||
break
|
||||
_goal_decision = evaluate_goal_after_turn(
|
||||
session_id,
|
||||
_last_goal_response,
|
||||
user_initiated=True,
|
||||
profile_home=_profile_home,
|
||||
)
|
||||
if not has_active_goal(session_id, profile_home=_profile_home):
|
||||
_goal_decision = {}
|
||||
else:
|
||||
_last_goal_response = ''
|
||||
for _goal_msg in reversed(s.messages or []):
|
||||
if not isinstance(_goal_msg, dict) or _goal_msg.get('role') != 'assistant':
|
||||
continue
|
||||
_goal_content = _goal_msg.get('content', '')
|
||||
if isinstance(_goal_content, list):
|
||||
_goal_parts = []
|
||||
for _goal_part in _goal_content:
|
||||
if isinstance(_goal_part, dict):
|
||||
_goal_text = _goal_part.get('text') or _goal_part.get('content')
|
||||
if _goal_text:
|
||||
_goal_parts.append(str(_goal_text))
|
||||
_last_goal_response = '\n'.join(_goal_parts)
|
||||
else:
|
||||
_last_goal_response = str(_goal_content or '')
|
||||
break
|
||||
put('goal', {
|
||||
'session_id': session_id,
|
||||
'state': 'evaluating',
|
||||
'message': 'Evaluating goal progress…',
|
||||
})
|
||||
_goal_decision = evaluate_goal_after_turn(
|
||||
session_id,
|
||||
_last_goal_response,
|
||||
user_initiated=True,
|
||||
profile_home=_profile_home,
|
||||
)
|
||||
decision = _goal_decision or {}
|
||||
_goal_message = str(decision.get('message') or '').strip()
|
||||
if _goal_message:
|
||||
put('goal', {
|
||||
'session_id': session_id,
|
||||
'state': 'continuing' if decision.get('should_continue') else 'idle',
|
||||
'message': _goal_message,
|
||||
'decision': decision,
|
||||
})
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
+7
-1
@@ -885,9 +885,15 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
try{
|
||||
const d=JSON.parse(e.data||'{}');
|
||||
if((d.session_id||activeSid)!==activeSid) return;
|
||||
const goalState=String(d.state||'').trim();
|
||||
const goalEvaluatingMessage='Evaluating goal progress…';
|
||||
if(goalState==='evaluating'){
|
||||
setComposerStatus(goalEvaluatingMessage);
|
||||
return;
|
||||
}
|
||||
const msg=String(d.message||'').trim();
|
||||
if(!msg)return;
|
||||
_latestGoalStatus={message:msg,decision:d.decision||null};
|
||||
_latestGoalStatus={message:msg,decision:d.decision||null,state:goalState||null};
|
||||
setComposerStatus(msg);
|
||||
showToast(msg.split('\n')[0],2600);
|
||||
}catch(_){}
|
||||
|
||||
@@ -100,6 +100,25 @@ def test_goal_command_payload_rejects_new_goal_while_stream_running(monkeypatch)
|
||||
assert "use /goal status / pause / clear mid-run" in rejected["message"]
|
||||
|
||||
|
||||
def test_has_active_goal_reports_only_active_state(monkeypatch):
|
||||
"""Streaming can avoid showing an evaluating spinner when no standing goal is active."""
|
||||
from api import goals as webui_goals
|
||||
|
||||
class FakeGoalManager:
|
||||
def __init__(self, session_id, default_max_turns=20):
|
||||
self.session_id = session_id
|
||||
|
||||
def is_active(self):
|
||||
return self.session_id == "sid-active-goal"
|
||||
|
||||
monkeypatch.setattr(webui_goals, "GoalManager", FakeGoalManager)
|
||||
monkeypatch.setattr(webui_goals, "_default_max_turns", lambda: 20)
|
||||
|
||||
assert webui_goals.has_active_goal("sid-active-goal") is True
|
||||
assert webui_goals.has_active_goal("sid-idle-goal") is False
|
||||
assert webui_goals.has_active_goal("") is False
|
||||
|
||||
|
||||
def test_goal_continuation_decision_emits_status_and_normal_user_prompt(monkeypatch):
|
||||
"""Post-turn hook returns the visible status event plus a normal continuation prompt."""
|
||||
from api import goals as webui_goals
|
||||
@@ -221,6 +240,17 @@ def test_streaming_post_turn_goal_hook_surfaces_and_continues():
|
||||
assert goal_idx < done_idx, "goal status should be emitted before the terminal done payload"
|
||||
|
||||
|
||||
def test_streaming_goal_hook_emits_evaluating_state_before_judge():
|
||||
evaluating_idx = STREAMING_PY.find("'state': 'evaluating'")
|
||||
judge_idx = STREAMING_PY.find("_goal_decision = evaluate_goal_after_turn")
|
||||
done_idx = STREAMING_PY.find("put('done'", judge_idx)
|
||||
assert evaluating_idx != -1, "goal hook should emit an evaluating state before judge round-trip"
|
||||
assert judge_idx != -1 and done_idx != -1
|
||||
assert evaluating_idx < judge_idx < done_idx
|
||||
assert "Evaluating goal progress…" in STREAMING_PY
|
||||
assert "'state': 'continuing' if decision.get('should_continue') else 'idle'" in STREAMING_PY
|
||||
|
||||
|
||||
def test_frontend_has_goal_slash_command_and_status_event_handler():
|
||||
assert "{name:'goal'" in COMMANDS_JS
|
||||
assert "subArgs:['status','pause','resume','clear']" in COMMANDS_JS
|
||||
@@ -232,3 +262,11 @@ def test_frontend_has_goal_slash_command_and_status_event_handler():
|
||||
assert "source.addEventListener('goal_continue'" in MESSAGES_JS
|
||||
assert "['steer','interrupt','queue','terminal','goal'].includes(_pc.name)" in MESSAGES_JS
|
||||
assert "queueSessionMessage" in MESSAGES_JS
|
||||
|
||||
|
||||
def test_frontend_goal_evaluating_state_uses_calm_composer_indicator():
|
||||
assert "const goalState=String(d.state||'').trim();" in MESSAGES_JS
|
||||
assert "const goalEvaluatingMessage='Evaluating goal progress…';" in MESSAGES_JS
|
||||
assert "if(goalState==='evaluating')" in MESSAGES_JS
|
||||
assert "setComposerStatus(goalEvaluatingMessage);" in MESSAGES_JS
|
||||
assert "return;" in MESSAGES_JS
|
||||
|
||||
Reference in New Issue
Block a user