alpiecode 0.9.2__tar.gz → 0.9.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {alpiecode-0.9.2 → alpiecode-0.9.4}/PKG-INFO +1 -1
  2. {alpiecode-0.9.2 → alpiecode-0.9.4}/pyproject.toml +1 -1
  3. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/alpiecode.egg-info/SOURCES.txt +1 -0
  5. alpiecode-0.9.4/src/codeagent/cache.py +195 -0
  6. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/extension/alpiecode.vsix +0 -0
  7. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/orchestrator.py +33 -1
  8. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/server.py +10 -1
  9. {alpiecode-0.9.2 → alpiecode-0.9.4}/README.md +0 -0
  10. {alpiecode-0.9.2 → alpiecode-0.9.4}/setup.cfg +0 -0
  11. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  12. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/alpiecode.egg-info/entry_points.txt +0 -0
  13. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/alpiecode.egg-info/requires.txt +0 -0
  14. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/alpiecode.egg-info/top_level.txt +0 -0
  15. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/__init__.py +0 -0
  16. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/agent.py +0 -0
  17. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/backends/__init__.py +0 -0
  18. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/backends/base.py +0 -0
  19. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/backends/local_backend.py +0 -0
  20. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/backends/openai_backend.py +0 -0
  21. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/cli.py +0 -0
  22. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/client.py +0 -0
  23. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/compaction.py +0 -0
  24. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/config.py +0 -0
  25. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/context.py +0 -0
  26. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/executor.py +0 -0
  27. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/github.py +0 -0
  28. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/guardian.py +0 -0
  29. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/local_model.py +0 -0
  30. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/media.py +0 -0
  31. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/memory.py +0 -0
  32. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/prompt.py +0 -0
  33. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/session.py +0 -0
  34. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/tools.py +0 -0
  35. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/updater.py +0 -0
  36. {alpiecode-0.9.2 → alpiecode-0.9.4}/src/codeagent/vscode_installer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 0.9.2
3
+ Version: 0.9.4
4
4
  Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpiecode"
7
- version = "0.9.2"
7
+ version = "0.9.4"
8
8
  description = "AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 0.9.2
3
+ Version: 0.9.4
4
4
  Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -8,6 +8,7 @@ src/alpiecode.egg-info/requires.txt
8
8
  src/alpiecode.egg-info/top_level.txt
9
9
  src/codeagent/__init__.py
10
10
  src/codeagent/agent.py
11
+ src/codeagent/cache.py
11
12
  src/codeagent/cli.py
12
13
  src/codeagent/client.py
13
14
  src/codeagent/compaction.py
@@ -0,0 +1,195 @@
1
+ """
2
+ Response cache for AlpieCode.
3
+
4
+ Caches LLM responses on disk (~/.alpiecode/cache/) so identical prompts
5
+ return instantly without model inference. Shared across CLI, IDE, and VS Code.
6
+
7
+ Design:
8
+ - Cache key = SHA-256 hash of normalized task text
9
+ - Only caches "pure" responses (no tool calls — those are context-dependent)
10
+ - In-memory LRU + disk persistence for instant lookups across restarts
11
+ - TTL: 7 days, Max entries: 500
12
+ """
13
+
14
+ import hashlib
15
+ import json
16
+ import time
17
+ from pathlib import Path
18
+ from typing import Any, Dict, Optional
19
+
20
+ CACHE_DIR = Path.home() / ".alpiecode" / "cache"
21
+ MAX_ENTRIES = 500
22
+ TTL_SECONDS = 7 * 24 * 3600 # 7 days
23
+
24
+
25
+ class ResponseCache:
26
+ """Disk-backed LRU response cache."""
27
+
28
+ def __init__(
29
+ self,
30
+ cache_dir: Path = CACHE_DIR,
31
+ max_entries: int = MAX_ENTRIES,
32
+ ttl: int = TTL_SECONDS,
33
+ ):
34
+ self.cache_dir = cache_dir
35
+ self.max_entries = max_entries
36
+ self.ttl = ttl
37
+ self._mem: Dict[str, Dict[str, Any]] = {}
38
+ self._hits = 0
39
+ self._misses = 0
40
+
41
+ try:
42
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
43
+ except OSError:
44
+ pass
45
+
46
+ self._warm_up()
47
+
48
+ # ── Public API ──────────────────────────────────────────────────────
49
+
50
+ def get(self, task: str) -> Optional[Dict[str, Any]]:
51
+ """
52
+ Look up a cached response for the given task.
53
+
54
+ Returns dict with keys: response, reasoning (optional), cached_at
55
+ Returns None on cache miss or expired entry.
56
+ """
57
+ key = self._key(task)
58
+
59
+ # Memory check (fast path)
60
+ entry = self._mem.get(key)
61
+ if entry and self._is_valid(entry):
62
+ self._hits += 1
63
+ return entry
64
+
65
+ # Disk check (slow path)
66
+ path = self._path(key)
67
+ if path.exists():
68
+ try:
69
+ entry = json.loads(path.read_text(encoding="utf-8"))
70
+ if self._is_valid(entry):
71
+ self._mem[key] = entry # Promote to memory
72
+ self._hits += 1
73
+ return entry
74
+ else:
75
+ path.unlink(missing_ok=True) # Expired
76
+ except Exception:
77
+ pass
78
+
79
+ self._misses += 1
80
+ return None
81
+
82
+ def put(self, task: str, response: str, reasoning: Optional[str] = None) -> None:
83
+ """
84
+ Store a response in the cache.
85
+
86
+ Only call this for responses that completed WITHOUT tool calls.
87
+ """
88
+ if not response or not task.strip():
89
+ return
90
+
91
+ key = self._key(task)
92
+ entry = {
93
+ "task": task.strip(),
94
+ "response": response,
95
+ "reasoning": reasoning,
96
+ "cached_at": time.time(),
97
+ }
98
+
99
+ self._mem[key] = entry
100
+
101
+ # Write to disk
102
+ try:
103
+ path = self._path(key)
104
+ path.write_text(json.dumps(entry, ensure_ascii=False, indent=2), encoding="utf-8")
105
+ except OSError:
106
+ pass
107
+
108
+ self._enforce_limit()
109
+
110
+ def clear(self) -> int:
111
+ """Clear all cached responses. Returns number of entries removed."""
112
+ count = 0
113
+ try:
114
+ for f in self.cache_dir.glob("*.json"):
115
+ f.unlink(missing_ok=True)
116
+ count += 1
117
+ except OSError:
118
+ pass
119
+ self._mem.clear()
120
+ return count
121
+
122
+ @property
123
+ def stats(self) -> Dict[str, Any]:
124
+ """Return cache statistics."""
125
+ disk_count = len(list(self.cache_dir.glob("*.json"))) if self.cache_dir.exists() else 0
126
+ total = self._hits + self._misses
127
+ return {
128
+ "entries": disk_count,
129
+ "memory": len(self._mem),
130
+ "hits": self._hits,
131
+ "misses": self._misses,
132
+ "hit_rate": f"{self._hits / total * 100:.1f}%" if total > 0 else "0%",
133
+ }
134
+
135
+ # ── Internals ───────────────────────────────────────────────────────
136
+
137
+ def _key(self, task: str) -> str:
138
+ """Normalize and hash the task text into a 16-char hex key."""
139
+ normalized = " ".join(task.strip().lower().split())
140
+ return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16]
141
+
142
+ def _path(self, key: str) -> Path:
143
+ return self.cache_dir / f"{key}.json"
144
+
145
+ def _is_valid(self, entry: Dict[str, Any]) -> bool:
146
+ cached_at = entry.get("cached_at", 0)
147
+ return (time.time() - cached_at) < self.ttl
148
+
149
+ def _warm_up(self):
150
+ """Load the 100 most recent entries into memory on startup."""
151
+ if not self.cache_dir.exists():
152
+ return
153
+ try:
154
+ files = sorted(
155
+ self.cache_dir.glob("*.json"),
156
+ key=lambda f: f.stat().st_mtime,
157
+ reverse=True,
158
+ )
159
+ for f in files[:100]:
160
+ try:
161
+ entry = json.loads(f.read_text(encoding="utf-8"))
162
+ if self._is_valid(entry):
163
+ self._mem[f.stem] = entry
164
+ else:
165
+ f.unlink(missing_ok=True) # Clean expired
166
+ except Exception:
167
+ pass
168
+ except OSError:
169
+ pass
170
+
171
+ def _enforce_limit(self):
172
+ """Evict oldest entries if over max_entries."""
173
+ if not self.cache_dir.exists():
174
+ return
175
+ try:
176
+ files = sorted(self.cache_dir.glob("*.json"), key=lambda f: f.stat().st_mtime)
177
+ while len(files) > self.max_entries:
178
+ oldest = files.pop(0)
179
+ key = oldest.stem
180
+ oldest.unlink(missing_ok=True)
181
+ self._mem.pop(key, None)
182
+ except OSError:
183
+ pass
184
+
185
+
186
+ # ── Singleton ───────────────────────────────────────────────────────────
187
+ _CACHE: Optional[ResponseCache] = None
188
+
189
+
190
+ def get_cache() -> ResponseCache:
191
+ """Get or create the global response cache singleton."""
192
+ global _CACHE
193
+ if _CACHE is None:
194
+ _CACHE = ResponseCache()
195
+ return _CACHE
@@ -13,6 +13,7 @@ from typing import Any, Dict, Iterator, Optional
13
13
  from .backends.base import ChatResponse, InferenceBackend, ToolCall
14
14
  from .backends.local_backend import LocalBackend
15
15
  from .backends.openai_backend import OpenAIBackend
16
+ from .cache import get_cache
16
17
  from .config import Config, is_server_reachable
17
18
  from .context import ContextManager
18
19
  from .executor import ToolExecutor, ToolResult
@@ -63,6 +64,29 @@ class AgentOrchestrator:
63
64
  """
64
65
  Run full agent task loop for a session. Yields AgentEvents.
65
66
  """
67
+ # ── Response cache check (instant return for repeated prompts) ──
68
+ # Only cache pure text prompts (no images, videos, URLs, or GitHub repos)
69
+ is_cacheable = not any([image_path, video_path, url, github_repo])
70
+ if is_cacheable:
71
+ cache = get_cache()
72
+ cached = cache.get(task)
73
+ if cached:
74
+ yield AgentEvent("start", {
75
+ "task": task,
76
+ "workdir": str(session.workdir),
77
+ "backend": "cache",
78
+ "is_offline": False,
79
+ "tool_count": 0,
80
+ })
81
+ yield AgentEvent("cache_hit", {
82
+ "message": "Returning cached response (instant)",
83
+ })
84
+ if cached.get("reasoning"):
85
+ yield AgentEvent("thinking", {"content": cached["reasoning"]})
86
+ yield AgentEvent("message", {"content": cached["response"]})
87
+ yield AgentEvent("done", {"summary": cached["response"]})
88
+ return
89
+
66
90
  # Dynamic backend re-check: if currently on LocalBackend but remote
67
91
  # API is now reachable, switch to OnlineBackend automatically.
68
92
  # This handles the case where the server started offline but the
@@ -176,8 +200,16 @@ class AgentOrchestrator:
176
200
 
177
201
  continue
178
202
 
179
- # Assistant text response
203
+ # Assistant text response (no tool calls = cacheable)
180
204
  if resp.content:
205
+ # Cache this response — it completed in a single turn without tools
206
+ if is_cacheable and turn == 0:
207
+ try:
208
+ cache = get_cache()
209
+ cache.put(task, resp.content, reasoning=resp.reasoning)
210
+ except Exception:
211
+ pass
212
+
181
213
  yield AgentEvent("message", {"content": resp.content})
182
214
  extract_and_save_memories(session.workdir, session.context.messages)
183
215
  yield AgentEvent("done", {"summary": resp.content})
@@ -69,12 +69,21 @@ if HAS_FASTAPI:
69
69
  backend_name = app.state.backend.name if hasattr(app.state, "backend") else "unknown"
70
70
  is_avail = app.state.backend.is_available if hasattr(app.state, "backend") else False
71
71
  uptime = time.time() - app.state.start_time if hasattr(app.state, "start_time") else 0.0
72
+
73
+ # Cache stats
74
+ try:
75
+ from .cache import get_cache
76
+ cache_stats = get_cache().stats
77
+ except Exception:
78
+ cache_stats = {}
79
+
72
80
  return {
73
81
  "status": "online",
74
82
  "backend": backend_name,
75
83
  "available": is_avail,
76
84
  "uptime_seconds": round(uptime, 2),
77
- "version": "0.7.2",
85
+ "version": "0.9.2",
86
+ "cache": cache_stats,
78
87
  }
79
88
 
80
89
  @app.get("/sessions")
File without changes
File without changes