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.<key>.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:df51ad797mtime-cached load_config/read_raw_config andc2eda92fdremoved 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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user