From 2c8ff7248ffde6b777197d8ef76c65b1cd2250fc Mon Sep 17 00:00:00 2001 From: w3rdist-creator Date: Tue, 2 Jun 2026 09:15:09 -0400 Subject: [PATCH] feat(gateway): add runtime footer turn count --- gateway/run.py | 6 +++ gateway/runtime_footer.py | 30 +++++++++++- tests/gateway/test_runtime_footer.py | 72 +++++++++++++++++++++++++++- 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index f11686ccd360b..799bfb7ef544c 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -9408,6 +9408,11 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g _footer_line = "" try: from gateway.runtime_footer import build_footer_line as _bfl + _turn_count = None + try: + _turn_count = sum(1 for _m in (history or []) if _m.get("role") == "user") + 1 + except Exception: + _turn_count = None _footer_line = _bfl( user_config=_load_gateway_config(), platform_key=_platform_config_key(source.platform), @@ -9415,6 +9420,7 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g context_tokens=agent_result.get("last_prompt_tokens", 0) or 0, context_length=agent_result.get("context_length") or None, cwd=os.environ.get("TERMINAL_CWD", ""), + turn_count=_turn_count, ) except Exception as _footer_err: logger.debug("runtime_footer build failed: %s", _footer_err) diff --git a/gateway/runtime_footer.py b/gateway/runtime_footer.py index 024cf74d68170..41f3fad6d1263 100644 --- a/gateway/runtime_footer.py +++ b/gateway/runtime_footer.py @@ -10,6 +10,8 @@ runtime_footer: enabled: true # off by default fields: [model, context_pct, cwd] # order shown; drop any to hide + # Optional: include ``turn_count`` for a subtle ``12/100`` session tracker. + turn_limit: 100 # denominator for turn_count Per-platform overrides live under ``display.platforms..runtime_footer``. Users can toggle the global setting with ``/footer on|off`` from both the CLI @@ -53,6 +55,17 @@ def _model_short(model: Optional[str]) -> str: return model.rsplit("/", 1)[-1] +def _coerce_positive_int(value: Any) -> Optional[int]: + """Return *value* as a positive int, or ``None`` when invalid/missing.""" + try: + coerced = int(value) + except (TypeError, ValueError): + return None + if coerced <= 0: + return None + return coerced + + def resolve_footer_config( user_config: dict[str, Any] | None, platform_key: str | None = None, @@ -64,7 +77,7 @@ def resolve_footer_config( 2. ``display.runtime_footer`` 3. ``display.platforms..runtime_footer`` """ - resolved = {"enabled": False, "fields": list(_DEFAULT_FIELDS)} + resolved = {"enabled": False, "fields": list(_DEFAULT_FIELDS), "turn_limit": 100} cfg = (user_config or {}).get("display") or {} global_cfg = cfg.get("runtime_footer") @@ -73,6 +86,9 @@ def resolve_footer_config( resolved["enabled"] = bool(global_cfg.get("enabled")) if isinstance(global_cfg.get("fields"), list) and global_cfg["fields"]: resolved["fields"] = [str(f) for f in global_cfg["fields"]] + turn_limit = _coerce_positive_int(global_cfg.get("turn_limit")) + if turn_limit is not None: + resolved["turn_limit"] = turn_limit if platform_key: platforms = cfg.get("platforms") or {} @@ -84,6 +100,9 @@ def resolve_footer_config( resolved["enabled"] = bool(plat_footer.get("enabled")) if isinstance(plat_footer.get("fields"), list) and plat_footer["fields"]: resolved["fields"] = [str(f) for f in plat_footer["fields"]] + turn_limit = _coerce_positive_int(plat_footer.get("turn_limit")) + if turn_limit is not None: + resolved["turn_limit"] = turn_limit return resolved @@ -94,6 +113,8 @@ def format_runtime_footer( context_tokens: int, context_length: Optional[int], cwd: Optional[str] = None, + turn_count: Optional[int] = None, + turn_limit: int = 100, fields: Iterable[str] = _DEFAULT_FIELDS, ) -> str: """Render the footer line, or return "" if no fields have data. @@ -115,6 +136,10 @@ def format_runtime_footer( rel = _home_relative_cwd(cwd or os.environ.get("TERMINAL_CWD", "")) if rel: parts.append(rel) + elif field == "turn_count": + if turn_count is not None and turn_limit > 0: + shown_count = max(1, int(turn_count)) + parts.append(f"{shown_count}/{int(turn_limit)}") # Unknown field names are silently ignored. if not parts: @@ -130,6 +155,7 @@ def build_footer_line( context_tokens: int, context_length: Optional[int], cwd: Optional[str] = None, + turn_count: Optional[int] = None, ) -> str: """Top-level entry point used by gateway/run.py. @@ -145,5 +171,7 @@ def build_footer_line( context_tokens=context_tokens, context_length=context_length, cwd=cwd, + turn_count=turn_count, + turn_limit=int(cfg.get("turn_limit") or 100), fields=cfg.get("fields") or _DEFAULT_FIELDS, ) diff --git a/tests/gateway/test_runtime_footer.py b/tests/gateway/test_runtime_footer.py index 9c36706f71b42..b43d23bbfbf5a 100644 --- a/tests/gateway/test_runtime_footer.py +++ b/tests/gateway/test_runtime_footer.py @@ -147,13 +147,36 @@ def test_format_footer_unknown_field_silently_ignored(): assert out == "gpt-5.4 ยท 50%" +def test_format_footer_turn_count_only(): + out = format_runtime_footer( + model="openai/gpt-5.4", + context_tokens=50, context_length=100, + cwd="/x", + turn_count=12, + fields=("turn_count",), + ) + assert out == "12/100" + + +def test_format_footer_turn_count_custom_limit(): + out = format_runtime_footer( + model="openai/gpt-5.4", + context_tokens=50, context_length=100, + cwd="/x", + turn_count=7, + turn_limit=50, + fields=("turn_count",), + ) + assert out == "7/50" + + # --------------------------------------------------------------------------- # resolve_footer_config # --------------------------------------------------------------------------- def test_resolve_defaults_off_empty_config(): cfg = resolve_footer_config({}, "telegram") - assert cfg == {"enabled": False, "fields": ["model", "context_pct", "cwd"]} + assert cfg == {"enabled": False, "fields": ["model", "context_pct", "cwd"], "turn_limit": 100} def test_resolve_global_enable(): @@ -163,6 +186,12 @@ def test_resolve_global_enable(): assert cfg["fields"] == ["model", "context_pct", "cwd"] +def test_resolve_global_turn_limit(): + user = {"display": {"runtime_footer": {"enabled": True, "turn_limit": 75}}} + cfg = resolve_footer_config(user, "telegram") + assert cfg["turn_limit"] == 75 + + def test_resolve_platform_override_wins(): user = { "display": { @@ -231,6 +260,47 @@ def test_build_footer_returns_rendered_when_enabled(monkeypatch, tmp_path): assert "25%" in out +def test_build_footer_turn_count_platform_override(): + out = build_footer_line( + user_config={ + "display": { + "platforms": { + "discord": { + "runtime_footer": {"enabled": True, "fields": ["turn_count"]} + } + } + } + }, + platform_key="discord", + model="openai/gpt-5.4", + context_tokens=25, + context_length=100, + cwd="/tmp", + turn_count=42, + ) + assert out == "42/100" + + +def test_build_footer_turn_count_uses_configured_platform_limit(): + out = build_footer_line( + user_config={ + "display": { + "runtime_footer": {"enabled": True, "fields": ["turn_count"], "turn_limit": 100}, + "platforms": { + "discord": {"runtime_footer": {"turn_limit": 250}} + }, + } + }, + platform_key="discord", + model="openai/gpt-5.4", + context_tokens=25, + context_length=100, + cwd="/tmp", + turn_count=42, + ) + assert out == "42/250" + + def test_build_footer_per_platform_off_suppresses(): user = { "display": {