mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 11:40:26 +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(
|
def evaluate_goal_after_turn(
|
||||||
session_id: str,
|
session_id: str,
|
||||||
last_response: str,
|
last_response: str,
|
||||||
|
|||||||
+32
-23
@@ -3106,35 +3106,44 @@ def _run_agent_streaming(
|
|||||||
# frontend surfaces the status line and queues continuation_prompt as
|
# frontend surfaces the status line and queues continuation_prompt as
|
||||||
# a normal next user message so /queue and user input keep priority.
|
# a normal next user message so /queue and user input keep priority.
|
||||||
try:
|
try:
|
||||||
from api.goals import evaluate_goal_after_turn
|
from api.goals import evaluate_goal_after_turn, has_active_goal
|
||||||
|
|
||||||
_last_goal_response = ''
|
if not has_active_goal(session_id, profile_home=_profile_home):
|
||||||
for _goal_msg in reversed(s.messages or []):
|
_goal_decision = {}
|
||||||
if not isinstance(_goal_msg, dict) or _goal_msg.get('role') != 'assistant':
|
else:
|
||||||
continue
|
_last_goal_response = ''
|
||||||
_goal_content = _goal_msg.get('content', '')
|
for _goal_msg in reversed(s.messages or []):
|
||||||
if isinstance(_goal_content, list):
|
if not isinstance(_goal_msg, dict) or _goal_msg.get('role') != 'assistant':
|
||||||
_goal_parts = []
|
continue
|
||||||
for _goal_part in _goal_content:
|
_goal_content = _goal_msg.get('content', '')
|
||||||
if isinstance(_goal_part, dict):
|
if isinstance(_goal_content, list):
|
||||||
_goal_text = _goal_part.get('text') or _goal_part.get('content')
|
_goal_parts = []
|
||||||
if _goal_text:
|
for _goal_part in _goal_content:
|
||||||
_goal_parts.append(str(_goal_text))
|
if isinstance(_goal_part, dict):
|
||||||
_last_goal_response = '\n'.join(_goal_parts)
|
_goal_text = _goal_part.get('text') or _goal_part.get('content')
|
||||||
else:
|
if _goal_text:
|
||||||
_last_goal_response = str(_goal_content or '')
|
_goal_parts.append(str(_goal_text))
|
||||||
break
|
_last_goal_response = '\n'.join(_goal_parts)
|
||||||
_goal_decision = evaluate_goal_after_turn(
|
else:
|
||||||
session_id,
|
_last_goal_response = str(_goal_content or '')
|
||||||
_last_goal_response,
|
break
|
||||||
user_initiated=True,
|
put('goal', {
|
||||||
profile_home=_profile_home,
|
'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 {}
|
decision = _goal_decision or {}
|
||||||
_goal_message = str(decision.get('message') or '').strip()
|
_goal_message = str(decision.get('message') or '').strip()
|
||||||
if _goal_message:
|
if _goal_message:
|
||||||
put('goal', {
|
put('goal', {
|
||||||
'session_id': session_id,
|
'session_id': session_id,
|
||||||
|
'state': 'continuing' if decision.get('should_continue') else 'idle',
|
||||||
'message': _goal_message,
|
'message': _goal_message,
|
||||||
'decision': decision,
|
'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{
|
try{
|
||||||
const d=JSON.parse(e.data||'{}');
|
const d=JSON.parse(e.data||'{}');
|
||||||
if((d.session_id||activeSid)!==activeSid) return;
|
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();
|
const msg=String(d.message||'').trim();
|
||||||
if(!msg)return;
|
if(!msg)return;
|
||||||
_latestGoalStatus={message:msg,decision:d.decision||null};
|
_latestGoalStatus={message:msg,decision:d.decision||null,state:goalState||null};
|
||||||
setComposerStatus(msg);
|
setComposerStatus(msg);
|
||||||
showToast(msg.split('\n')[0],2600);
|
showToast(msg.split('\n')[0],2600);
|
||||||
}catch(_){}
|
}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"]
|
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):
|
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."""
|
"""Post-turn hook returns the visible status event plus a normal continuation prompt."""
|
||||||
from api import goals as webui_goals
|
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"
|
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():
|
def test_frontend_has_goal_slash_command_and_status_event_handler():
|
||||||
assert "{name:'goal'" in COMMANDS_JS
|
assert "{name:'goal'" in COMMANDS_JS
|
||||||
assert "subArgs:['status','pause','resume','clear']" 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 "source.addEventListener('goal_continue'" in MESSAGES_JS
|
||||||
assert "['steer','interrupt','queue','terminal','goal'].includes(_pc.name)" in MESSAGES_JS
|
assert "['steer','interrupt','queue','terminal','goal'].includes(_pc.name)" in MESSAGES_JS
|
||||||
assert "queueSessionMessage" 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