diff --git a/agent/display.py b/agent/display.py index 9d5796987..382ca4746 100644 --- a/agent/display.py +++ b/agent/display.py @@ -657,10 +657,6 @@ def format_context_pressure( The bar and percentage show progress toward the compaction threshold, NOT the raw context window. 100% = compaction fires. - Uses ANSI colors: - - cyan at ~60% to compaction = informational - - bold yellow at ~85% to compaction = warning - Args: compaction_progress: How close to compaction (0.0–1.0, 1.0 = fires). threshold_tokens: Compaction threshold in tokens. @@ -674,18 +670,12 @@ def format_context_pressure( threshold_k = f"{threshold_tokens // 1000}k" if threshold_tokens >= 1000 else str(threshold_tokens) threshold_pct_int = int(threshold_percent * 100) - # Tier styling - if compaction_progress >= 0.85: - color = f"{_BOLD}{_YELLOW}" - icon = "⚠" - if compression_enabled: - hint = "compaction imminent" - else: - hint = "no auto-compaction" + color = f"{_BOLD}{_YELLOW}" + icon = "⚠" + if compression_enabled: + hint = "compaction approaching" else: - color = _CYAN - icon = "◐" - hint = "approaching compaction" + hint = "no auto-compaction" return ( f" {color}{icon} context {bar} {pct_int}% to compaction{_ANSI_RESET}" @@ -709,14 +699,10 @@ def format_context_pressure_gateway( threshold_pct_int = int(threshold_percent * 100) - if compaction_progress >= 0.85: - icon = "⚠️" - if compression_enabled: - hint = f"Context compaction is imminent (threshold: {threshold_pct_int}% of window)." - else: - hint = "Auto-compaction is disabled — context may be truncated." + icon = "⚠️" + if compression_enabled: + hint = f"Context compaction approaching (threshold: {threshold_pct_int}% of window)." else: - icon = "ℹ️" - hint = f"Compaction threshold is at {threshold_pct_int}% of context window." + hint = "Auto-compaction is disabled — context may be truncated." return f"{icon} Context: {bar} {pct_int}% to compaction\n{hint}" diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 478a6acd5..ff460ebc9 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -873,9 +873,9 @@ def setup_model_provider(config: dict): keep_label = None # No provider configured — don't show "Keep current" provider_choices = [ + "OpenRouter API key (100+ models, pay-per-use)", "Login with Nous Portal (Nous Research subscription — OAuth)", "Login with OpenAI Codex", - "OpenRouter API key (100+ models, pay-per-use)", "Custom OpenAI-compatible endpoint (self-hosted / VLLM / etc.)", "Z.AI / GLM (Zhipu AI models)", "Kimi / Moonshot (Kimi coding models)", @@ -894,7 +894,7 @@ def setup_model_provider(config: dict): provider_choices.append(keep_label) # Default to "Keep current" if a provider exists, otherwise OpenRouter (most common) - default_provider = len(provider_choices) - 1 if has_any_provider else 2 + default_provider = len(provider_choices) - 1 if has_any_provider else 0 if not has_any_provider: print_warning("An inference provider is required for Hermes to work.") @@ -911,81 +911,7 @@ def setup_model_provider(config: dict): selected_base_url = None # deferred until after model selection nous_models = [] # populated if Nous login succeeds - if provider_idx == 0: # Nous Portal (OAuth) - selected_provider = "nous" - print() - print_header("Nous Portal Login") - print_info("This will open your browser to authenticate with Nous Portal.") - print_info("You'll need a Nous Research account with an active subscription.") - print() - - try: - from hermes_cli.auth import _login_nous, ProviderConfig - import argparse - - mock_args = argparse.Namespace( - portal_url=None, - inference_url=None, - client_id=None, - scope=None, - no_browser=False, - timeout=15.0, - ca_bundle=None, - insecure=False, - ) - pconfig = PROVIDER_REGISTRY["nous"] - _login_nous(mock_args, pconfig) - _sync_model_from_disk(config) - - # Fetch models for the selection step - try: - creds = resolve_nous_runtime_credentials( - min_key_ttl_seconds=5 * 60, - timeout_seconds=15.0, - ) - nous_models = fetch_nous_models( - inference_base_url=creds.get("base_url", ""), - api_key=creds.get("api_key", ""), - ) - except Exception as e: - logger.debug("Could not fetch Nous models after login: %s", e) - - except SystemExit: - print_warning("Nous Portal login was cancelled or failed.") - print_info("You can try again later with: hermes model") - selected_provider = None - except Exception as e: - print_error(f"Login failed: {e}") - print_info("You can try again later with: hermes model") - selected_provider = None - - elif provider_idx == 1: # OpenAI Codex - selected_provider = "openai-codex" - print() - print_header("OpenAI Codex Login") - print() - - try: - import argparse - - mock_args = argparse.Namespace() - _login_openai_codex(mock_args, PROVIDER_REGISTRY["openai-codex"]) - # Clear custom endpoint vars that would override provider routing. - if existing_custom: - save_env_value("OPENAI_BASE_URL", "") - save_env_value("OPENAI_API_KEY", "") - _update_config_for_provider("openai-codex", DEFAULT_CODEX_BASE_URL) - _set_model_provider(config, "openai-codex", DEFAULT_CODEX_BASE_URL) - except SystemExit: - print_warning("OpenAI Codex login was cancelled or failed.") - print_info("You can try again later with: hermes model") - selected_provider = None - except Exception as e: - print_error(f"Login failed: {e}") - print_info("You can try again later with: hermes model") - selected_provider = None - - elif provider_idx == 2: # OpenRouter + if provider_idx == 0: # OpenRouter selected_provider = "openrouter" print() print_header("OpenRouter API Key") @@ -1040,6 +966,80 @@ def setup_model_provider(config: dict): except Exception as e: logger.debug("Could not save provider to config.yaml: %s", e) + elif provider_idx == 1: # Nous Portal (OAuth) + selected_provider = "nous" + print() + print_header("Nous Portal Login") + print_info("This will open your browser to authenticate with Nous Portal.") + print_info("You'll need a Nous Research account with an active subscription.") + print() + + try: + from hermes_cli.auth import _login_nous, ProviderConfig + import argparse + + mock_args = argparse.Namespace( + portal_url=None, + inference_url=None, + client_id=None, + scope=None, + no_browser=False, + timeout=15.0, + ca_bundle=None, + insecure=False, + ) + pconfig = PROVIDER_REGISTRY["nous"] + _login_nous(mock_args, pconfig) + _sync_model_from_disk(config) + + # Fetch models for the selection step + try: + creds = resolve_nous_runtime_credentials( + min_key_ttl_seconds=5 * 60, + timeout_seconds=15.0, + ) + nous_models = fetch_nous_models( + inference_base_url=creds.get("base_url", ""), + api_key=creds.get("api_key", ""), + ) + except Exception as e: + logger.debug("Could not fetch Nous models after login: %s", e) + + except SystemExit: + print_warning("Nous Portal login was cancelled or failed.") + print_info("You can try again later with: hermes model") + selected_provider = None + except Exception as e: + print_error(f"Login failed: {e}") + print_info("You can try again later with: hermes model") + selected_provider = None + + elif provider_idx == 2: # OpenAI Codex + selected_provider = "openai-codex" + print() + print_header("OpenAI Codex Login") + print() + + try: + import argparse + + mock_args = argparse.Namespace() + _login_openai_codex(mock_args, PROVIDER_REGISTRY["openai-codex"]) + # Clear custom endpoint vars that would override provider routing. + if existing_custom: + save_env_value("OPENAI_BASE_URL", "") + save_env_value("OPENAI_API_KEY", "") + _update_config_for_provider("openai-codex", DEFAULT_CODEX_BASE_URL) + _set_model_provider(config, "openai-codex", DEFAULT_CODEX_BASE_URL) + except SystemExit: + print_warning("OpenAI Codex login was cancelled or failed.") + print_info("You can try again later with: hermes model") + selected_provider = None + except Exception as e: + print_error(f"Login failed: {e}") + print_info("You can try again later with: hermes model") + selected_provider = None + elif provider_idx == 3: # Custom endpoint selected_provider = "custom" print() diff --git a/mini-swe-agent b/mini-swe-agent new file mode 160000 index 000000000..07aa6a738 --- /dev/null +++ b/mini-swe-agent @@ -0,0 +1 @@ +Subproject commit 07aa6a738556e44b30d7b5c3bbd5063dac871d25 diff --git a/run_agent.py b/run_agent.py index 08e2807b8..13b5e2d3c 100644 --- a/run_agent.py +++ b/run_agent.py @@ -585,8 +585,7 @@ class AIAgent: # Context pressure warnings: notify the USER (not the LLM) as context # fills up. Purely informational — displayed in CLI output and sent via # status_callback for gateway platforms. Does NOT inject into messages. - self._context_50_warned = False - self._context_70_warned = False + self._context_pressure_warned = False # Persistent error log -- always writes WARNING+ to ~/.hermes/logs/errors.log # so tool failures, API errors, etc. are inspectable after the fact. @@ -4609,12 +4608,11 @@ class AIAgent: except Exception as e: logger.debug("Session DB compression split failed: %s", e) - # Reset context pressure warnings and token estimate — usage drops + # Reset context pressure warning and token estimate — usage drops # after compaction. Without this, the stale last_prompt_tokens from # the previous API call causes the pressure calculation to stay at # >1000% and spam warnings / re-trigger compression in a loop. - self._context_50_warned = False - self._context_70_warned = False + self._context_pressure_warned = False _compressed_est = ( estimate_tokens_rough(new_system_prompt) + estimate_messages_tokens_rough(compressed) @@ -6853,12 +6851,8 @@ class AIAgent: # and fires status_callback for gateway platforms. if _compressor.threshold_tokens > 0: _compaction_progress = _estimated_next_prompt / _compressor.threshold_tokens - if _compaction_progress >= 0.85 and not self._context_70_warned: - self._context_70_warned = True - self._context_50_warned = True # skip first tier if we jumped past it - self._emit_context_pressure(_compaction_progress, _compressor) - elif _compaction_progress >= 0.60 and not self._context_50_warned: - self._context_50_warned = True + if _compaction_progress >= 0.85 and not self._context_pressure_warned: + self._context_pressure_warned = True self._emit_context_pressure(_compaction_progress, _compressor) if self.compression_enabled and _compressor.should_compress(_estimated_next_prompt): diff --git a/tests/hermes_cli/test_setup.py b/tests/hermes_cli/test_setup.py index ee2f9d90c..a4c85ba2b 100644 --- a/tests/hermes_cli/test_setup.py +++ b/tests/hermes_cli/test_setup.py @@ -34,7 +34,7 @@ def test_nous_oauth_setup_keeps_current_model_when_syncing_disk_provider( def fake_prompt_choice(question, choices, default=0): if question == "Select your inference provider:": - return 0 + return 1 # Nous Portal if question == "Configure vision:": return len(choices) - 1 if question == "Select default model:": @@ -135,7 +135,7 @@ def test_codex_setup_uses_runtime_access_token_for_live_model_list(tmp_path, mon def fake_prompt_choice(question, choices, default=0): if question == "Select your inference provider:": - return 1 + return 2 # OpenAI Codex if question == "Select default model:": return 0 tts_idx = _maybe_keep_current_tts(question, choices) diff --git a/tests/hermes_cli/test_setup_model_provider.py b/tests/hermes_cli/test_setup_model_provider.py index 39f3a1feb..0acbfea51 100644 --- a/tests/hermes_cli/test_setup_model_provider.py +++ b/tests/hermes_cli/test_setup_model_provider.py @@ -401,7 +401,7 @@ def test_setup_switch_custom_to_codex_clears_custom_endpoint_and_updates_config( def fake_prompt_choice(question, choices, default=0): if question == "Select your inference provider:": - return 1 + return 2 # OpenAI Codex if question == "Select default model:": return 0 tts_idx = _maybe_keep_current_tts(question, choices) diff --git a/tests/test_context_pressure.py b/tests/test_context_pressure.py index 3d6b19026..f89daef52 100644 --- a/tests/test_context_pressure.py +++ b/tests/test_context_pressure.py @@ -29,40 +29,36 @@ class TestFormatContextPressure: raw context window. 60% = 60% of the way to compaction. """ - def test_60_percent_uses_info_icon(self): - line = format_context_pressure(0.60, 100_000, 0.50) - assert "◐" in line - assert "60% to compaction" in line - - def test_85_percent_uses_warning_icon(self): - line = format_context_pressure(0.85, 100_000, 0.50) + def test_80_percent_uses_warning_icon(self): + line = format_context_pressure(0.80, 100_000, 0.50) assert "⚠" in line - assert "85% to compaction" in line + assert "80% to compaction" in line + + def test_90_percent_uses_warning_icon(self): + line = format_context_pressure(0.90, 100_000, 0.50) + assert "⚠" in line + assert "90% to compaction" in line def test_bar_length_scales_with_progress(self): - line_60 = format_context_pressure(0.60, 100_000, 0.50) - line_85 = format_context_pressure(0.85, 100_000, 0.50) - assert line_85.count("▰") > line_60.count("▰") + line_80 = format_context_pressure(0.80, 100_000, 0.50) + line_95 = format_context_pressure(0.95, 100_000, 0.50) + assert line_95.count("▰") > line_80.count("▰") def test_shows_threshold_tokens(self): - line = format_context_pressure(0.60, 100_000, 0.50) + line = format_context_pressure(0.80, 100_000, 0.50) assert "100k" in line def test_small_threshold(self): - line = format_context_pressure(0.60, 500, 0.50) + line = format_context_pressure(0.80, 500, 0.50) assert "500" in line def test_shows_threshold_percent(self): - line = format_context_pressure(0.85, 100_000, 0.50) - assert "50%" in line # threshold percent shown + line = format_context_pressure(0.80, 100_000, 0.50) + assert "50%" in line - def test_imminent_hint_at_85(self): - line = format_context_pressure(0.85, 100_000, 0.50) - assert "compaction imminent" in line - - def test_approaching_hint_below_85(self): - line = format_context_pressure(0.60, 100_000, 0.80) - assert "approaching compaction" in line + def test_approaching_hint(self): + line = format_context_pressure(0.80, 100_000, 0.50) + assert "compaction approaching" in line def test_no_compaction_when_disabled(self): line = format_context_pressure(0.85, 100_000, 0.50, compression_enabled=False) @@ -82,26 +78,26 @@ class TestFormatContextPressure: class TestFormatContextPressureGateway: """Gateway (plain text) context pressure display.""" - def test_60_percent_informational(self): - msg = format_context_pressure_gateway(0.60, 0.50) - assert "60% to compaction" in msg - assert "50%" in msg # threshold shown + def test_80_percent_warning(self): + msg = format_context_pressure_gateway(0.80, 0.50) + assert "80% to compaction" in msg + assert "50%" in msg - def test_85_percent_warning(self): - msg = format_context_pressure_gateway(0.85, 0.50) - assert "85% to compaction" in msg - assert "imminent" in msg + def test_90_percent_warning(self): + msg = format_context_pressure_gateway(0.90, 0.50) + assert "90% to compaction" in msg + assert "approaching" in msg def test_no_compaction_warning(self): msg = format_context_pressure_gateway(0.85, 0.50, compression_enabled=False) assert "disabled" in msg def test_no_ansi_codes(self): - msg = format_context_pressure_gateway(0.85, 0.50) + msg = format_context_pressure_gateway(0.80, 0.50) assert "\033[" not in msg def test_has_progress_bar(self): - msg = format_context_pressure_gateway(0.85, 0.50) + msg = format_context_pressure_gateway(0.80, 0.50) assert "▰" in msg @@ -145,9 +141,8 @@ def agent(): class TestContextPressureFlags: """Context pressure warning flag tracking on AIAgent.""" - def test_flags_initialized_false(self, agent): - assert agent._context_50_warned is False - assert agent._context_70_warned is False + def test_flag_initialized_false(self, agent): + assert agent._context_pressure_warned is False def test_emit_calls_status_callback(self, agent): """status_callback should be invoked with event type and message.""" @@ -204,13 +199,11 @@ class TestContextPressureFlags: captured = capsys.readouterr() assert "▰" not in captured.out - def test_flags_reset_on_compression(self, agent): - """After _compress_context, context pressure flags should reset.""" - agent._context_50_warned = True - agent._context_70_warned = True + def test_flag_reset_on_compression(self, agent): + """After _compress_context, context pressure flag should reset.""" + agent._context_pressure_warned = True agent.compression_enabled = True - # Mock the compressor's compress method to return minimal valid output agent.context_compressor = MagicMock() agent.context_compressor.compress.return_value = [ {"role": "user", "content": "Summary of conversation so far."} @@ -218,11 +211,9 @@ class TestContextPressureFlags: agent.context_compressor.context_length = 200_000 agent.context_compressor.threshold_tokens = 100_000 - # Mock _todo_store agent._todo_store = MagicMock() agent._todo_store.format_for_injection.return_value = None - # Mock _build_system_prompt agent._build_system_prompt = MagicMock(return_value="system prompt") agent._cached_system_prompt = "old system prompt" agent._session_db = None @@ -233,8 +224,7 @@ class TestContextPressureFlags: ] agent._compress_context(messages, "system prompt") - assert agent._context_50_warned is False - assert agent._context_70_warned is False + assert agent._context_pressure_warned is False def test_emit_callback_error_handled(self, agent): """If status_callback raises, it should be caught gracefully."""