From 003b4c8893245ed96fef8b344c9e89c3e218cd3e Mon Sep 17 00:00:00 2001 From: 314574126 <314574126@qq.com> Date: Mon, 3 Aug 2026 21:06:51 +0530 Subject: [PATCH] perf(gateway): per-platform skip_context_files to cut agent build latency Salvage of #26860 (hunk 2, ported \u2014 the PR's base predates the current gateway layout by ~11.9K commits). Messaging platforms can set gateway.platforms..skip_context_files: true to skip the filesystem-heavy context-file discovery (SOUL.md, AGENTS.md, .cursorrules walks) during AIAgent construction \u2014 10-100x slower stat()/walk costs on Windows made this a real per-turn tax. Soul identity is still loaded (single small file), so the persona survives. The flag participates in _agent_config_signature so toggling it rebuilds the cached agent instead of silently reusing a prompt built under the other setting (prompt-cache correctness). The PR's hunk 1 (mtime-caching the per-turn dotenv reload) was dropped: df51ad797 mtime-cached load_config/read_raw_config and c2eda92fd removed the per-turn deepcopies, capturing most of that win; the function has since gained a multiplex early-return and managed-scope overlay that the original whole-function skip would have bypassed. --- gateway/run.py | 21 +++++ .../gateway/test_skip_context_files_wiring.py | 83 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 tests/gateway/test_skip_context_files_wiring.py diff --git a/gateway/run.py b/gateway/run.py index 4df2227ae4..a0ceaa2cbc 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -4447,6 +4447,17 @@ class TurnRunner: turn_route = self._runner._resolve_turn_agent_config(ctx.message, model, runtime_kwargs) + # Per-platform skip_context_files — messaging platforms can opt out + # of filesystem-heavy context-file discovery (SOUL.md, AGENTS.md, + # .cursorrules) to cut AIAgent construction latency. Especially + # impactful on Windows, where stat() + directory walks are 10-100x + # slower than Linux. Off by default; soul identity is preserved so + # the persona survives even with minimal context. + _platforms_gw_cfg = (ctx.user_config.get("gateway") or {}).get("platforms") or {} + _plat_gw_cfg = _platforms_gw_cfg.get(platform_key) or {} + _skip_context = _plat_gw_cfg.get("skip_context_files") + skip_context_files = bool(_skip_context) if _skip_context is not None else False + # Check agent cache — reuse the AIAgent from the previous message # in this session to preserve the frozen system prompt and tool # schemas for prompt cache hits. @@ -4458,6 +4469,7 @@ class TurnRunner: cache_keys=self._runner._extract_cache_busting_config(ctx.user_config), user_id=getattr(ctx.source, "user_id", None), user_id_alt=getattr(ctx.source, "user_id_alt", None), + skip_context_files=skip_context_files, ) agent = None reused_cached_agent = False @@ -4693,6 +4705,10 @@ class TurnRunner: session_db=getattr(self._runner._session_db, "_db", self._runner._session_db), # Reload from disk — do not reuse the startup snapshot (#60955). fallback_model=self._runner._refresh_fallback_model(), + skip_context_files=skip_context_files, + # Keep the persona even with minimal context: soul identity is + # a single small file, not part of the expensive walk. + load_soul_identity=True, ) if _cache_lock and _cache is not None: with _cache_lock: @@ -22341,6 +22357,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew cache_keys: dict | None = None, user_id: str | None = None, user_id_alt: str | None = None, + skip_context_files: bool = False, ) -> str: """Compute a stable string key from agent config values. @@ -22395,6 +22412,10 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew _cache_keys_sorted, str(user_id or ""), str(user_id_alt or ""), + # skip_context_files changes the agent's frozen system prompt + # (context files in vs out) — a toggled config edit must + # rebuild the cached agent, not silently reuse it. + bool(skip_context_files), ], sort_keys=True, default=str, diff --git a/tests/gateway/test_skip_context_files_wiring.py b/tests/gateway/test_skip_context_files_wiring.py new file mode 100644 index 0000000000..eaea6babf8 --- /dev/null +++ b/tests/gateway/test_skip_context_files_wiring.py @@ -0,0 +1,83 @@ +"""Per-platform ``skip_context_files`` gateway wiring (#26860). + +Messaging platforms can opt out of the filesystem-heavy context-file +discovery (SOUL.md, AGENTS.md, .cursorrules walks) that runs during +AIAgent construction — especially impactful on Windows where stat() and +directory walks are 10-100x slower. The agent-side parameters already +exist (agent/agent_init.py); these tests pin the gateway wiring: +config -> signature -> AIAgent kwargs. +""" + +import pytest + +from gateway.run import GatewayRunner + + +class TestSkipContextFilesSignature: + """A toggled skip_context_files must invalidate the agent cache.""" + + RUNTIME = {"provider": "openrouter", "base_url": "", "api_mode": ""} + + def test_signature_differs_when_toggled(self): + sig_off = GatewayRunner._agent_config_signature( + "claude-sonnet-4", self.RUNTIME, ["hermes-telegram"], "", + skip_context_files=False, + ) + sig_on = GatewayRunner._agent_config_signature( + "claude-sonnet-4", self.RUNTIME, ["hermes-telegram"], "", + skip_context_files=True, + ) + assert sig_off != sig_on, ( + "skip_context_files changes the frozen system prompt (context " + "files in vs out) — the cache signature must change with it" + ) + + def test_signature_stable_when_unchanged(self): + sig_a = GatewayRunner._agent_config_signature( + "claude-sonnet-4", self.RUNTIME, ["hermes-telegram"], "", + skip_context_files=True, + ) + sig_b = GatewayRunner._agent_config_signature( + "claude-sonnet-4", self.RUNTIME, ["hermes-telegram"], "", + skip_context_files=True, + ) + assert sig_a == sig_b + + def test_default_matches_explicit_false(self): + """Back-compat: omitting the param must hash like False so existing + cached agents aren't all invalidated by this change.""" + sig_default = GatewayRunner._agent_config_signature( + "claude-sonnet-4", self.RUNTIME, ["hermes-telegram"], "", + ) + sig_false = GatewayRunner._agent_config_signature( + "claude-sonnet-4", self.RUNTIME, ["hermes-telegram"], "", + skip_context_files=False, + ) + assert sig_default == sig_false + + +class TestSkipContextFilesConfigResolution: + """The gateway resolution path: platform config dict -> bool.""" + + @pytest.mark.parametrize( + ("cfg", "platform_key", "expected"), + [ + ({"gateway": {"platforms": {"telegram": {"skip_context_files": True}}}}, "telegram", True), + ({"gateway": {"platforms": {"telegram": {"skip_context_files": False}}}}, "telegram", False), + ({"gateway": {"platforms": {"telegram": {}}}}, "telegram", False), + ({"gateway": {"platforms": {}}}, "telegram", False), + ({"gateway": {}}, "telegram", False), + ({}, "telegram", False), + # Set on a DIFFERENT platform — must not leak. + ({"gateway": {"platforms": {"discord": {"skip_context_files": True}}}}, "telegram", False), + # Truthy non-bool values coerce. + ({"gateway": {"platforms": {"telegram": {"skip_context_files": 1}}}}, "telegram", True), + ], + ) + def test_resolution(self, cfg, platform_key, expected): + # Mirror the production resolution in TurnRunner exactly. + _platforms_gw_cfg = (cfg.get("gateway") or {}).get("platforms") or {} + _plat_gw_cfg = _platforms_gw_cfg.get(platform_key) or {} + _skip_context = _plat_gw_cfg.get("skip_context_files") + skip_context_files = bool(_skip_context) if _skip_context is not None else False + assert skip_context_files is expected