fix: surface goal evaluation status

This commit is contained in:
Michael Lam
2026-05-08 08:20:24 -07:00
parent ab847cb680
commit f2aacf4ba0
5 changed files with 96 additions and 24 deletions
+19
View File
@@ -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
View File
@@ -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
View File
@@ -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(_){}
+38
View File
@@ -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