fix: show auto-compression running state

This commit is contained in:
Michael Lam
2026-05-07 11:24:52 -07:00
committed by nesquena-hermes
parent ac8a41bc1f
commit e31b7e72d6
4 changed files with 95 additions and 0 deletions

View File

@@ -1886,6 +1886,29 @@ def _run_agent_streaming(
except Exception:
logger.debug("Failed to put event to queue")
def _agent_status_callback(kind, message):
"""Bridge Agent lifecycle compression status into WebUI SSE."""
_message = str(message or '').strip()
_kind = str(kind or '').strip().lower()
if not _message:
return
_lower = _message.lower()
_is_compression_start = (
_kind == 'lifecycle'
and (
'preflight compression' in _lower
or 'compressing' in _lower
or 'compacting context' in _lower
or 'context too large' in _lower
)
)
if not _is_compression_start:
return
put('compressing', {
'session_id': session_id,
'message': 'Auto-compressing context to continue...',
})
# Initialised here (before any code that may raise) so the outer `finally`
# block can safely check `if _checkpoint_stop is not None` even when an
# exception fires before the checkpoint thread is created (Issue #765).
@@ -2330,6 +2353,8 @@ def _run_agent_streaming(
# but guard defensively to avoid TypeError on an older agent build.
if 'reasoning_config' in _agent_params and _reasoning_config is not None:
_agent_kwargs['reasoning_config'] = _reasoning_config
if 'status_callback' in _agent_params:
_agent_kwargs['status_callback'] = _agent_status_callback
if 'max_tokens' in _agent_params and _max_tokens_cfg is not None:
_agent_kwargs['max_tokens'] = _max_tokens_cfg
# Params added in newer hermes-agent — skip if not supported
@@ -2383,6 +2408,8 @@ def _run_agent_streaming(
# objects (put queue, cancel_event) that are new each request.
agent.stream_delta_callback = _agent_kwargs.get('stream_delta_callback')
agent.tool_progress_callback = _agent_kwargs.get('tool_progress_callback')
if hasattr(agent, 'status_callback'):
agent.status_callback = _agent_kwargs.get('status_callback')
if hasattr(agent, 'reasoning_callback'):
agent.reasoning_callback = _agent_kwargs.get('reasoning_callback')
if hasattr(agent, 'clarify_callback'):

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -1011,6 +1011,24 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
}catch(_){}
});
source.addEventListener('compressing',e=>{
// Context auto-compression is starting. Surface the same calm running
// compression card as manual /compress while the summarizer LLM call runs.
if(!S.session||S.session.session_id!==activeSid) return;
let d={};
try{ d=JSON.parse(e.data||'{}')||{}; }catch(_){ d={}; }
if(d.session_id&&d.session_id!==activeSid) return;
if(typeof setCompressionUi==='function'){
setCompressionUi({
sessionId:activeSid,
phase:'running',
automatic:true,
message:d.message||'Auto-compressing context...',
});
}
if(typeof renderMessages==='function') renderMessages({preserveScroll:true});
});
source.addEventListener('compressed',e=>{
// Context was auto-compressed during this turn. Render it through the
// same transient compression-card path as manual /compress, without

View File

@@ -17,6 +17,56 @@ def _compressed_listener_block() -> str:
return src[start:end]
def _compressing_listener_block() -> str:
src = _read("static/messages.js")
start = src.find("source.addEventListener('compressing'")
assert start != -1, "compressing SSE listener not found"
end = src.find("source.addEventListener('compressed'", start)
assert end != -1, "compressed listener after compressing SSE listener not found"
return src[start:end]
def test_auto_compression_running_sse_uses_active_session_running_card():
block = _compressing_listener_block()
assert "if(!S.session||S.session.session_id!==activeSid) return;" in block
assert "if(d.session_id&&d.session_id!==activeSid) return;" in block
assert "try{ d=JSON.parse(e.data||'{}')||{}; }catch(_){ d={}; }" in block
assert "setCompressionUi" in block
assert "phase:'running'" in block
assert "automatic:true" in block
assert "message:d.message||'Auto-compressing context...'" in block
def test_auto_compression_running_sse_is_emitted_from_agent_lifecycle_status():
src = _read("api/streaming.py")
start = src.find("def _agent_status_callback")
assert start != -1, "agent status callback bridge not found"
end = src.find("# Initialised here", start)
assert end != -1, "status callback block end marker not found"
block = src[start:end]
assert "put('compressing'" in block
assert "'session_id': session_id" in block
assert "'message': 'Auto-compressing context to continue...'" in block
assert "'preflight compression'" in block
assert "'compressing'" in block
assert "'compacting context'" in block
assert "'context too large'" in block
assert "'status_callback' in _agent_params" in src
assert "_agent_kwargs['status_callback'] = _agent_status_callback" in src
assert "agent.status_callback = _agent_kwargs.get('status_callback')" in src
def test_auto_compression_completion_transition_is_preserved_after_running_listener():
src = _read("static/messages.js")
compressing_idx = src.find("source.addEventListener('compressing'")
compressed_idx = src.find("source.addEventListener('compressed'")
assert compressing_idx != -1 and compressed_idx != -1
assert compressing_idx < compressed_idx
assert "phase:'done'" in _compressed_listener_block()
def test_auto_compression_sse_uses_transient_card_not_fake_message():
"""Auto compression must not inject display-only text into S.messages."""
src = _read("static/messages.js")