diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 2a210434949b..8e061f831b8e 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -350,7 +350,13 @@ def _strip_yaml_frontmatter(content: str) -> str: ), "cli": ( "You are a CLI AI Agent. Try not to use markdown but simple text " - "renderable inside a terminal." + "renderable inside a terminal. " + "File delivery: there is no attachment channel — the user reads your " + "response directly in their terminal. Do NOT emit MEDIA:/path tags " + "(those are only intercepted on messaging platforms like Telegram, " + "Discord, Slack, etc.; on the CLI they render as literal text). " + "When referring to a file you created or changed, just state its " + "absolute path in plain text; the user can open it from there." ), "sms": ( "You are communicating via SMS. Keep responses concise and use plain text " diff --git a/hermes_cli/model_normalize.py b/hermes_cli/model_normalize.py index 76dace065a3a..b1781162c5d5 100644 --- a/hermes_cli/model_normalize.py +++ b/hermes_cli/model_normalize.py @@ -408,6 +408,23 @@ def normalize_model_for_provider(model_input: str, target_provider: str) -> str: return bare return _normalize_for_deepseek(bare) + # --- Kimi / Moonshot: map auto-discovered short IDs (k2p6) to API-accepted + # canonical names (kimi-k2.6). The /models endpoint returns internal + # short forms that fail at request time without this repair (issue #13758). + if provider in {"kimi-coding", "kimi-coding-cn", "moonshot"}: + bare = _strip_matching_provider_prefix(name, provider) + if "/" in bare: + bare = bare.split("/", 1)[1] + # k2p6 -> kimi-k2.6, k2p5 -> kimi-k2.5, etc. + _kimi_short_map = { + "k2p6": "kimi-k2.6", + "k2p5": "kimi-k2.5", + "k2p8": "kimi-k2.8", + } + if bare.lower() in _kimi_short_map: + return _kimi_short_map[bare.lower()] + return bare + # --- Direct providers: repair matching provider prefixes only --- if provider in _MATCHING_PREFIX_STRIP_PROVIDERS: return _strip_matching_provider_prefix(name, provider) diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 36b3c7f3f395..521e891e7e03 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -80,12 +80,20 @@ def _get_effective_configurable_toolsets(): Plugin toolsets are appended at the end so they appear after the built-in toolsets in the TUI checklist. + Duplicate keys (plugin toolsets whose key already exists in + CONFIGURABLE_TOOLSETS) are skipped so the built-in row is kept — + plugin-added tools are extensions of that toolset, not a second + top-level row (issue #13640). """ result = list(CONFIGURABLE_TOOLSETS) + built_in_keys = {ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS} try: from hermes_cli.plugins import discover_plugins, get_plugin_toolsets discover_plugins() # idempotent — ensures plugins are loaded - result.extend(get_plugin_toolsets()) + plugin_toolsets = get_plugin_toolsets() + # Skip plugin toolsets whose key already exists in built-ins + plugin_toolsets = [(k, l, d) for k, l, d in plugin_toolsets if k not in built_in_keys] + result.extend(plugin_toolsets) except Exception: pass return result diff --git a/run_agent.py b/run_agent.py index c5966a173706..8bb4b265ae2f 100644 --- a/run_agent.py +++ b/run_agent.py @@ -2178,11 +2178,58 @@ def _check_compression_model_feasibility(self) -> None: aux_base_url = str(getattr(client, "base_url", "")) aux_api_key = str(getattr(client, "api_key", "")) + # If no explicit override, look up context_length from + # providers / custom_providers for the auxiliary model — the + # same lookup that the main model uses in __init__. + _aux_ctx = getattr(self, "_aux_compression_context_length_config", None) + if _aux_ctx is None and aux_base_url: + try: + from hermes_cli.config import get_compatible_custom_providers + _custom_providers = get_compatible_custom_providers(_agent_cfg) + except Exception: + _custom_providers = _agent_cfg.get("custom_providers", []) + for _cp_entry in (_custom_providers or []): + if not isinstance(_cp_entry, dict): + continue + _cp_url = (_cp_entry.get("base_url") or "").rstrip("/") + if _cp_url and _cp_url == aux_base_url.rstrip("/"): + _cp_models = _cp_entry.get("models", {}) + if isinstance(_cp_models, dict): + _cp_model_cfg = _cp_models.get(aux_model, {}) + if isinstance(_cp_model_cfg, dict): + _cp_ctx = _cp_model_cfg.get("context_length") + if _cp_ctx is not None: + try: + _aux_ctx = int(_cp_ctx) + except (ValueError, TypeError): + pass + if _aux_ctx is not None: + break + # Also check the keyed providers schema (providers..models..context_length) + if _aux_ctx is None: + _providers = _agent_cfg.get("providers", {}) + if isinstance(_providers, dict): + for _pk, _pentry in _providers.items(): + if not isinstance(_pentry, dict): + continue + _purl = (_pentry.get("base_url") or "").rstrip("/") + if _purl and _purl == aux_base_url.rstrip("/"): + _pm = _pentry.get("models", {}) + if isinstance(_pm, dict): + _pm_cfg = _pm.get(aux_model, {}) + if isinstance(_pm_cfg, dict): + _pm_ctx = _pm_cfg.get("context_length") + if _pm_ctx is not None: + try: + _aux_ctx = int(_pm_ctx) + except (ValueError, TypeError): + pass + aux_context = get_model_context_length( aux_model, base_url=aux_base_url, api_key=aux_api_key, - config_context_length=getattr(self, "_aux_compression_context_length_config", None), + config_context_length=_aux_ctx, ) # Hard floor: the auxiliary compression model must have at least diff --git a/tests/agent/test_prompt_builder.py b/tests/agent/test_prompt_builder.py index 0962060313bb..11712b95192d 100644 --- a/tests/agent/test_prompt_builder.py +++ b/tests/agent/test_prompt_builder.py @@ -789,6 +789,24 @@ def test_platform_hints_known_platforms(self): assert "cron" in PLATFORM_HINTS assert "cli" in PLATFORM_HINTS + def test_cli_hint_does_not_suggest_media_tags(self): + # Regression: MEDIA:/path tags are intercepted only by messaging + # gateway platforms. On the CLI they render as literal text and + # confuse users. The CLI hint must steer the agent away from them. + cli_hint = PLATFORM_HINTS["cli"] + assert "MEDIA:" in cli_hint, ( + "CLI hint should mention MEDIA: in order to tell the agent " + "NOT to use it (negative guidance)." + ) + # Must contain explicit "don't" language near the MEDIA reference. + assert any( + marker in cli_hint.lower() + for marker in ("do not emit media", "not intercepted", "do not", "don't") + ), "CLI hint should explicitly discourage MEDIA: tags." + # Messaging hints should still advertise MEDIA: positively (sanity + # check that this test is calibrated correctly). + assert "include MEDIA:" in PLATFORM_HINTS["telegram"] + # ========================================================================= # Environment hints diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx index 28f7b324e2f9..f7d2aee19832 100644 --- a/ui-tui/src/components/appChrome.tsx +++ b/ui-tui/src/components/appChrome.tsx @@ -131,7 +131,10 @@ export function StatusRule({ ) : ( {status} )} - │ {model} + + + {model} + {ctxLabel ? │ {ctxLabel} : null} {bar ? (