Skip to content

Commit 3ca4605

Browse files
teknium1Test
andauthored
feat: context pressure warnings for CLI and gateway (NousResearch#2159)
* feat: context pressure warnings for CLI and gateway User-facing notifications as context approaches the compaction threshold. Warnings fire at 60% and 85% of the way to compaction β€” relative to the configured compression threshold, not the raw context window. CLI: Formatted line with a progress bar showing distance to compaction. Cyan at 60% (approaching), bold yellow at 85% (imminent). ◐ context β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–±β–±β–±β–±β–±β–±β–±β–± 60% to compaction 100k threshold (50%) Β· approaching compaction ⚠ context β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–°β–±β–±β–± 85% to compaction 100k threshold (50%) Β· compaction imminent Gateway: Plain-text notification sent to the user's chat via the new status_callback mechanism (asyncio.run_coroutine_threadsafe bridge, same pattern as step_callback). Does NOT inject into the message stream. The LLM never sees these warnings. Flags reset after each compaction cycle. Files changed: - agent/display.py β€” format_context_pressure(), format_context_pressure_gateway() - run_agent.py β€” status_callback param, _context_50/70_warned flags, _emit_context_pressure(), flag reset in _compress_context() - gateway/run.py β€” _status_callback_sync bridge, wired to AIAgent - tests/test_context_pressure.py β€” 23 tests * Merge remote-tracking branch 'origin/main' into hermes/hermes-7ea545bf --------- Co-authored-by: Test <test@test.com>
1 parent 7d129ee commit 3ca4605

4 files changed

Lines changed: 430 additions & 0 deletions

File tree

β€Žagent/display.pyβ€Ž

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,3 +612,95 @@ def write_tty(text: str) -> None:
612612
except OSError:
613613
sys.stdout.write(text)
614614
sys.stdout.flush()
615+
616+
617+
# =========================================================================
618+
# Context pressure display (CLI user-facing warnings)
619+
# =========================================================================
620+
621+
# ANSI color codes for context pressure tiers
622+
_CYAN = "\033[36m"
623+
_YELLOW = "\033[33m"
624+
_BOLD = "\033[1m"
625+
_DIM_ANSI = "\033[2m"
626+
627+
# Bar characters
628+
_BAR_FILLED = "β–°"
629+
_BAR_EMPTY = "β–±"
630+
_BAR_WIDTH = 20
631+
632+
633+
def format_context_pressure(
634+
compaction_progress: float,
635+
threshold_tokens: int,
636+
threshold_percent: float,
637+
compression_enabled: bool = True,
638+
) -> str:
639+
"""Build a formatted context pressure line for CLI display.
640+
641+
The bar and percentage show progress toward the compaction threshold,
642+
NOT the raw context window. 100% = compaction fires.
643+
644+
Uses ANSI colors:
645+
- cyan at ~60% to compaction = informational
646+
- bold yellow at ~85% to compaction = warning
647+
648+
Args:
649+
compaction_progress: How close to compaction (0.0–1.0, 1.0 = fires).
650+
threshold_tokens: Compaction threshold in tokens.
651+
threshold_percent: Compaction threshold as a fraction of context window.
652+
compression_enabled: Whether auto-compression is active.
653+
"""
654+
pct_int = int(compaction_progress * 100)
655+
filled = min(int(compaction_progress * _BAR_WIDTH), _BAR_WIDTH)
656+
bar = _BAR_FILLED * filled + _BAR_EMPTY * (_BAR_WIDTH - filled)
657+
658+
threshold_k = f"{threshold_tokens // 1000}k" if threshold_tokens >= 1000 else str(threshold_tokens)
659+
threshold_pct_int = int(threshold_percent * 100)
660+
661+
# Tier styling
662+
if compaction_progress >= 0.85:
663+
color = f"{_BOLD}{_YELLOW}"
664+
icon = "⚠"
665+
if compression_enabled:
666+
hint = "compaction imminent"
667+
else:
668+
hint = "no auto-compaction"
669+
else:
670+
color = _CYAN
671+
icon = "◐"
672+
hint = "approaching compaction"
673+
674+
return (
675+
f" {color}{icon} context {bar} {pct_int}% to compaction{_ANSI_RESET}"
676+
f" {_DIM_ANSI}{threshold_k} threshold ({threshold_pct_int}%) Β· {hint}{_ANSI_RESET}"
677+
)
678+
679+
680+
def format_context_pressure_gateway(
681+
compaction_progress: float,
682+
threshold_percent: float,
683+
compression_enabled: bool = True,
684+
) -> str:
685+
"""Build a plain-text context pressure notification for messaging platforms.
686+
687+
No ANSI β€” just Unicode and plain text suitable for Telegram/Discord/etc.
688+
The percentage shows progress toward the compaction threshold.
689+
"""
690+
pct_int = int(compaction_progress * 100)
691+
filled = min(int(compaction_progress * _BAR_WIDTH), _BAR_WIDTH)
692+
bar = _BAR_FILLED * filled + _BAR_EMPTY * (_BAR_WIDTH - filled)
693+
694+
threshold_pct_int = int(threshold_percent * 100)
695+
696+
if compaction_progress >= 0.85:
697+
icon = "⚠️"
698+
if compression_enabled:
699+
hint = f"Context compaction is imminent (threshold: {threshold_pct_int}% of window)."
700+
else:
701+
hint = "Auto-compaction is disabled β€” context may be truncated."
702+
else:
703+
icon = "ℹ️"
704+
hint = f"Compaction threshold is at {threshold_pct_int}% of context window."
705+
706+
return f"{icon} Context: {bar} {pct_int}% to compaction\n{hint}"

β€Žgateway/run.pyβ€Ž

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4539,6 +4539,26 @@ def _step_callback_sync(iteration: int, tool_names: list) -> None:
45394539
except Exception as _e:
45404540
logger.debug("agent:step hook error: %s", _e)
45414541

4542+
# Bridge sync status_callback β†’ async adapter.send for context pressure
4543+
_status_adapter = self.adapters.get(source.platform)
4544+
_status_chat_id = source.chat_id
4545+
_status_thread_metadata = {"thread_id": source.thread_id} if source.thread_id else None
4546+
4547+
def _status_callback_sync(event_type: str, message: str) -> None:
4548+
if not _status_adapter:
4549+
return
4550+
try:
4551+
asyncio.run_coroutine_threadsafe(
4552+
_status_adapter.send(
4553+
_status_chat_id,
4554+
message,
4555+
metadata=_status_thread_metadata,
4556+
),
4557+
_loop_for_step,
4558+
)
4559+
except Exception as _e:
4560+
logger.debug("status_callback error (%s): %s", event_type, _e)
4561+
45424562
def run_sync():
45434563
# Pass session_key to process registry via env var so background
45444564
# processes can be mapped back to this gateway session
@@ -4631,6 +4651,7 @@ def run_sync():
46314651
tool_progress_callback=progress_callback if tool_progress_enabled else None,
46324652
step_callback=_step_callback_sync if _hooks_ref.loaded_hooks else None,
46334653
stream_delta_callback=_stream_delta_cb,
4654+
status_callback=_status_callback_sync,
46344655
platform=platform_key,
46354656
honcho_session_key=session_key,
46364657
honcho_manager=honcho_manager,

β€Žrun_agent.pyβ€Ž

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ def __init__(
400400
clarify_callback: callable = None,
401401
step_callback: callable = None,
402402
stream_delta_callback: callable = None,
403+
status_callback: callable = None,
403404
max_tokens: int = None,
404405
reasoning_config: Dict[str, Any] = None,
405406
prefill_messages: List[Dict[str, Any]] = None,
@@ -522,6 +523,7 @@ def __init__(
522523
self.clarify_callback = clarify_callback
523524
self.step_callback = step_callback
524525
self.stream_delta_callback = stream_delta_callback
526+
self.status_callback = status_callback
525527
self._last_reported_tool = None # Track for "new tool" mode
526528

527529
# Tool execution state β€” allows _vprint during tool execution
@@ -571,6 +573,12 @@ def __init__(
571573
self._budget_warning_threshold = 0.9 # 90% β€” urgent, respond now
572574
self._budget_pressure_enabled = True
573575

576+
# Context pressure warnings: notify the USER (not the LLM) as context
577+
# fills up. Purely informational β€” displayed in CLI output and sent via
578+
# status_callback for gateway platforms. Does NOT inject into messages.
579+
self._context_50_warned = False
580+
self._context_70_warned = False
581+
574582
# Persistent error log -- always writes WARNING+ to ~/.hermes/logs/errors.log
575583
# so tool failures, API errors, etc. are inspectable after the fact.
576584
# In gateway mode, each incoming message creates a new AIAgent instance,
@@ -4385,6 +4393,10 @@ def _compress_context(self, messages: list, system_message: str, *, approx_token
43854393
except Exception as e:
43864394
logger.debug("Session DB compression split failed: %s", e)
43874395

4396+
# Reset context pressure warnings β€” usage drops after compaction
4397+
self._context_50_warned = False
4398+
self._context_70_warned = False
4399+
43884400
return compressed, new_system_prompt
43894401

43904402
def _execute_tool_calls(self, assistant_message, messages: list, effective_task_id: str, api_call_count: int = 0) -> None:
@@ -4965,6 +4977,45 @@ def _get_budget_warning(self, api_call_count: int) -> Optional[str]:
49654977
)
49664978
return None
49674979

4980+
def _emit_context_pressure(self, compaction_progress: float, compressor) -> None:
4981+
"""Notify the user that context is approaching the compaction threshold.
4982+
4983+
Args:
4984+
compaction_progress: How close to compaction (0.0–1.0, where 1.0 = fires).
4985+
compressor: The ContextCompressor instance (for threshold/context info).
4986+
4987+
Purely user-facing β€” does NOT modify the message stream.
4988+
For CLI: prints a formatted line with a progress bar.
4989+
For gateway: fires status_callback so the platform can send a chat message.
4990+
"""
4991+
from agent.display import format_context_pressure, format_context_pressure_gateway
4992+
4993+
threshold_pct = compressor.threshold_tokens / compressor.context_length if compressor.context_length else 0.5
4994+
4995+
# CLI output β€” always shown (these are user-facing status notifications,
4996+
# not verbose debug output, so they bypass quiet_mode).
4997+
# Gateway users also get the callback below.
4998+
if self.platform in (None, "cli"):
4999+
line = format_context_pressure(
5000+
compaction_progress=compaction_progress,
5001+
threshold_tokens=compressor.threshold_tokens,
5002+
threshold_percent=threshold_pct,
5003+
compression_enabled=self.compression_enabled,
5004+
)
5005+
self._safe_print(line)
5006+
5007+
# Gateway / external consumers
5008+
if self.status_callback:
5009+
try:
5010+
msg = format_context_pressure_gateway(
5011+
compaction_progress=compaction_progress,
5012+
threshold_percent=threshold_pct,
5013+
compression_enabled=self.compression_enabled,
5014+
)
5015+
self.status_callback("context_pressure", msg)
5016+
except Exception:
5017+
logger.debug("status_callback error in context pressure", exc_info=True)
5018+
49685019
def _handle_max_iterations(self, messages: list, api_call_count: int) -> str:
49695020
"""Request a summary when max iterations are reached. Returns the final response text."""
49705021
print(f"⚠️ Reached maximum iterations ({self.max_iterations}). Requesting summary...")
@@ -6540,6 +6591,23 @@ def _stop_spinner():
65406591
+ _compressor.last_completion_tokens
65416592
+ _new_chars // 3 # conservative: JSON-heavy tool results β‰ˆ 3 chars/token
65426593
)
6594+
6595+
# ── Context pressure warnings (user-facing only) ──────────
6596+
# Notify the user (NOT the LLM) as context approaches the
6597+
# compaction threshold. Thresholds are relative to where
6598+
# compaction fires, not the raw context window.
6599+
# Does not inject into messages β€” just prints to CLI output
6600+
# and fires status_callback for gateway platforms.
6601+
if _compressor.threshold_tokens > 0:
6602+
_compaction_progress = _estimated_next_prompt / _compressor.threshold_tokens
6603+
if _compaction_progress >= 0.85 and not self._context_70_warned:
6604+
self._context_70_warned = True
6605+
self._context_50_warned = True # skip first tier if we jumped past it
6606+
self._emit_context_pressure(_compaction_progress, _compressor)
6607+
elif _compaction_progress >= 0.60 and not self._context_50_warned:
6608+
self._context_50_warned = True
6609+
self._emit_context_pressure(_compaction_progress, _compressor)
6610+
65436611
if self.compression_enabled and _compressor.should_compress(_estimated_next_prompt):
65446612
messages, active_system_prompt = self._compress_context(
65456613
messages, system_message,

0 commit comments

Comments
Β (0)