Skip to content

Commit 0733da0

Browse files
Speed up REPL autocomplete with threaded TTL caches
Before cold/warm: agent 418.722/1.116ms, set 5.819/2.841ms, slash 410.299/7.811ms, cd 0.005/0.003ms, model 2.095/0.733ms, plain 0.003/0.001ms. After prewarm first/second: agent 0.172/0.027ms, set 13.474/9.531ms, slash 0.108/0.046ms, cd 0.010/0.007ms, model 3.427/0.083ms, plain 0.005/0.003ms. Completion runs off-loop via ThreadedCompleter.
1 parent ea0a9b7 commit 0733da0

6 files changed

Lines changed: 236 additions & 109 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Tiny thread-safe TTL caches for interactive completion lookups."""
2+
3+
from __future__ import annotations
4+
5+
import threading
6+
import time
7+
from collections.abc import Callable
8+
from typing import Generic, TypeVar
9+
10+
T = TypeVar("T")
11+
12+
13+
class TTLCache(Generic[T]):
14+
"""Cache one value briefly, coalescing concurrent cold loads."""
15+
16+
def __init__(self, ttl: float = 4.0, clock: Callable[[], float] = time.monotonic):
17+
self.ttl = ttl
18+
self.clock = clock
19+
self._value: T | None = None
20+
self._deadline = 0.0
21+
self._lock = threading.Lock()
22+
23+
def get(self, loader: Callable[[], T]) -> T:
24+
now = self.clock()
25+
if self._value is not None and now < self._deadline:
26+
return self._value
27+
with self._lock:
28+
now = self.clock()
29+
if self._value is None or now >= self._deadline:
30+
self._value = loader()
31+
self._deadline = now + self.ttl
32+
return self._value
33+
34+
def clear(self) -> None:
35+
with self._lock:
36+
self._value = None
37+
self._deadline = 0.0

code_puppy/command_line/model_picker_completion.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from termflow.tui.menu import MenuResult
1212

1313
from code_puppy.callbacks import on_prompt_toolkit_style
14+
from code_puppy.command_line.completion_cache import TTLCache
1415
from code_puppy.command_line.menu_session import menu_session
1516
from code_puppy.command_line.tui_style import themed
1617
from code_puppy.command_line.utils import safe_input
@@ -27,14 +28,20 @@
2728
logger = logging.getLogger(__name__)
2829

2930
MODEL_PICKER_PAGE_SIZE = 15
31+
_models_config_cache: TTLCache[dict] = TTLCache()
3032

3133

32-
def _load_models_config() -> dict:
34+
def _read_models_config() -> dict:
3335
from code_puppy.model_factory import ModelFactory
3436

3537
return ModelFactory.load_config()
3638

3739

40+
def _load_models_config() -> dict:
41+
"""Return the merged model config, refreshing it after a short TTL."""
42+
return _models_config_cache.get(_read_models_config)
43+
44+
3845
def load_model_names():
3946
"""Load model names from the config that's fetched from the endpoint."""
4047
models_config = _load_models_config()

code_puppy/command_line/pin_command_completion.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
from prompt_toolkit.completion import Completer, Completion
55
from prompt_toolkit.document import Document
66

7+
from code_puppy.command_line.completion_cache import TTLCache
8+
9+
_agent_names_cache: TTLCache[tuple[tuple[object, object], list[str]]] = TTLCache()
10+
_agent_meta_caches: dict[str, TTLCache[tuple[object, str]]] = {}
11+
712

813
def _get_json_agents_for_model(model_name: str) -> list:
914
"""Get JSON agents that have this model pinned in their JSON file."""
@@ -71,15 +76,28 @@ def _get_model_display_meta(model_name: str) -> str:
7176
return "Model"
7277

7378

74-
def _get_agent_display_meta(agent_name: str) -> str:
75-
"""Get display meta for an agent showing pinned model."""
79+
def _read_agent_display_meta(agent_name: str) -> str:
7680
pinned_model = _get_pinned_model_for_agent(agent_name)
7781
if pinned_model:
7882
return f"→ {pinned_model}"
7983
return "default"
8084

8185

82-
def load_agent_names():
86+
def _get_agent_display_meta(agent_name: str) -> str:
87+
"""Get briefly cached display meta for an agent's pinned model."""
88+
cache = _agent_meta_caches.setdefault(agent_name, TTLCache())
89+
source, meta = cache.get(
90+
lambda: (_get_pinned_model_for_agent, _read_agent_display_meta(agent_name))
91+
)
92+
if source is not _get_pinned_model_for_agent:
93+
cache.clear()
94+
_, meta = cache.get(
95+
lambda: (_get_pinned_model_for_agent, _read_agent_display_meta(agent_name))
96+
)
97+
return meta
98+
99+
100+
def _read_agent_names() -> list[str]:
83101
"""Load all available agent names (both built-in and JSON agents)."""
84102
agents = set()
85103

@@ -104,6 +122,21 @@ def load_agent_names():
104122
return sorted(list(agents))
105123

106124

125+
def load_agent_names() -> list[str]:
126+
"""Return available agent names, refreshing after a short TTL."""
127+
from code_puppy.agents.agent_manager import get_agent_descriptions
128+
from code_puppy.agents.json_agent import discover_json_agents
129+
130+
signature = (get_agent_descriptions, discover_json_agents)
131+
cached_signature, names = _agent_names_cache.get(
132+
lambda: (signature, _read_agent_names())
133+
)
134+
if cached_signature != signature:
135+
_agent_names_cache.clear()
136+
_, names = _agent_names_cache.get(lambda: (signature, _read_agent_names()))
137+
return names
138+
139+
107140
def load_model_names():
108141
"""Load model names from the config."""
109142
try:

0 commit comments

Comments
 (0)