alpiecode 10.0.2__tar.gz → 10.0.3__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 (61) hide show
  1. {alpiecode-10.0.2 → alpiecode-10.0.3}/PKG-INFO +1 -1
  2. {alpiecode-10.0.2 → alpiecode-10.0.3}/README.md +86 -12
  3. {alpiecode-10.0.2 → alpiecode-10.0.3}/pyproject.toml +1 -1
  4. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode.egg-info/PKG-INFO +1 -1
  5. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode.egg-info/SOURCES.txt +3 -0
  6. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/__init__.py +1 -1
  7. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/backends/local_backend.py +25 -1
  8. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/cache.py +39 -8
  9. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/cli.py +6 -1
  10. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/local_model.py +41 -9
  11. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/memory.py +23 -4
  12. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/metrics.py +19 -1
  13. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/orchestrator.py +9 -0
  14. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/prompt.py +5 -1
  15. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/tools.py +16 -2
  16. alpiecode-10.0.3/test/test_math_tools.py +214 -0
  17. alpiecode-10.0.3/test/test_offline_opt.py +59 -0
  18. alpiecode-10.0.3/test/test_palindrome_check.py +50 -0
  19. {alpiecode-10.0.2 → alpiecode-10.0.3}/setup.cfg +0 -0
  20. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode/__init__.py +0 -0
  21. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  22. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode.egg-info/entry_points.txt +0 -0
  23. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode.egg-info/requires.txt +0 -0
  24. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode.egg-info/top_level.txt +0 -0
  25. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/agent.py +0 -0
  26. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/backends/__init__.py +0 -0
  27. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/backends/base.py +0 -0
  28. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/backends/openai_backend.py +0 -0
  29. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/client.py +0 -0
  30. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/compaction.py +0 -0
  31. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/config.py +0 -0
  32. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/context.py +0 -0
  33. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/discovery.py +0 -0
  34. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/doctor.py +0 -0
  35. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/executor.py +0 -0
  36. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/extension/alpiecode.vsix +0 -0
  37. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/git_ops.py +0 -0
  38. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/github.py +0 -0
  39. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/guardian.py +0 -0
  40. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/guardrails.py +0 -0
  41. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/ipython_ext.py +0 -0
  42. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/media.py +0 -0
  43. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/progress.py +0 -0
  44. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/rephraser.py +0 -0
  45. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/server.py +0 -0
  46. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/session.py +0 -0
  47. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/system1.py +0 -0
  48. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/updater.py +0 -0
  49. {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/vscode_installer.py +0 -0
  50. {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_audit_fixes.py +0 -0
  51. {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_benchmark_system1.py +0 -0
  52. {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_cache_reads.py +0 -0
  53. {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_chat_mode.py +0 -0
  54. {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_claude_codex_capabilities.py +0 -0
  55. {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_context_filter.py +0 -0
  56. {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_metrics.py +0 -0
  57. {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_offline_mode.py +0 -0
  58. {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_phase1.py +0 -0
  59. {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_phase2.py +0 -0
  60. {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_phase3.py +0 -0
  61. {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_system1.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 10.0.2
3
+ Version: 10.0.3
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -1,7 +1,7 @@
1
1
  # AlpieCode
2
2
 
3
3
  [![Python Version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://www.python.org/)
4
- [![Version](https://img.shields.io/badge/alpiecode-v9.0.1-emerald.svg)](https://github.com/169Pi/codeagent-poc)
4
+ [![Version](https://img.shields.io/badge/alpiecode-v10.0.2-emerald.svg)](https://github.com/169Pi/codeagent-poc)
5
5
  [![VS Code Extension](https://img.shields.io/badge/vscode%20extension-v8.0.8-blueviolet.svg)](https://marketplace.visualstudio.com/)
6
6
  [![FastAPI](https://img.shields.io/badge/FastAPI-0.100%2B-009688.svg)](https://fastapi.tiangolo.com/)
7
7
  [![Jupyter & Colab](https://img.shields.io/badge/Jupyter%20%7C%20Colab-Ready-f37726.svg)](https://colab.research.google.com/)
@@ -16,16 +16,18 @@
16
16
  ## 📑 Table of Contents
17
17
  1. [🌟 Overview & Capabilities](#-overview--capabilities)
18
18
  2. [🏗️ Core Architecture & Hybrid Dual-Engine](#️-core-architecture--hybrid-dual-engine)
19
- 3. [📦 Complete Installation Guide](#-complete-installation-guide)
20
- 4. [🚀 Backend API Server (`alpiecode serve`)](#-backend-api-server-alpiecode-serve)
21
- 5. [⌨️ Command-Line Interface (CLI) Full Reference](#️-command-line-interface-cli-full-reference)
22
- 6. [💻 VS Code Extension Deep-Dive](#-vs-code-extension-deep-dive)
23
- 7. [🐍 Python SDK Reference (Programmatic Usage)](#-python-sdk-reference-programmatic-usage)
24
- 8. [📓 Jupyter Notebooks, JupyterLab & Google Colab](#-jupyter-notebooks-jupyterlab--google-colab)
25
- 9. [🛠️ 15 Built-In Autonomous Agent Tools](#️-15-built-in-autonomous-agent-tools)
26
- 10. [🛡️ Safety Guardian, Git Checkpoints & Memory System](#️-safety-guardian-git-checkpoints--memory-system)
27
- 11. [⚙️ Configuration & Environment Variables](#️-configuration--environment-variables)
28
- 12. [❓ FAQ & Troubleshooting](#-faq--troubleshooting)
19
+ 3. [⚡ System 1 (Jev Architecture) & Performance Engine](#-system-1-jev-architecture--performance-engine)
20
+ 4. [📊 Performance & Optimization Benchmarks](#-performance--optimization-benchmarks)
21
+ 5. [📦 Complete Installation Guide](#-complete-installation-guide)
22
+ 6. [🚀 Backend API Server (`alpiecode serve`)](#-backend-api-server-alpiecode-serve)
23
+ 7. [⌨️ Command-Line Interface (CLI) Full Reference](#️-command-line-interface-cli-full-reference)
24
+ 8. [💻 VS Code Extension Deep-Dive](#-vs-code-extension-deep-dive)
25
+ 9. [🐍 Python SDK Reference (Programmatic Usage)](#-python-sdk-reference-programmatic-usage)
26
+ 10. [📓 Jupyter Notebooks, JupyterLab & Google Colab](#-jupyter-notebooks-jupyterlab--google-colab)
27
+ 11. [🛠️ 15 Built-In Autonomous Agent Tools](#️-15-built-in-autonomous-agent-tools)
28
+ 12. [🛡️ Safety Guardian, Git Checkpoints & Memory System](#️-safety-guardian-git-checkpoints--memory-system)
29
+ 13. [⚙️ Configuration & Environment Variables](#️-configuration--environment-variables)
30
+ 14. [❓ FAQ & Troubleshooting](#-faq--troubleshooting)
29
31
 
30
32
  ---
31
33
 
@@ -122,6 +124,78 @@ AlpieCode employs an intelligent **Hybrid Dual-Engine** routing system that pair
122
124
 
123
125
  ---
124
126
 
127
+ ## ⚡ System 1 (Jev Architecture) & Performance Engine
128
+
129
+ AlpieCode introduces a **Dual-Process Cognitive Architecture** inspired by TypeSafe AI's **Jev (System 1)** framework:
130
+ - **System 1 (Fast Reflex & Decision Plane)**: A sub-5ms non-autoregressive decision engine that handles routine routing, parameter validation, test parsing, loop prevention, and error triage.
131
+ - **System 2 (Deep Thought & Generative Engine)**: The heavy reasoning LLM (169Pi Cloud VLA / Claude / GPT-4o online, or local Qwen GGUF offline) dedicated purely to code synthesis and architectural reasoning.
132
+
133
+ ```
134
+ ┌──────────────────────────────────────────────┐
135
+ │ User Coding Task │
136
+ └──────────────────────┬───────────────────────┘
137
+ │
138
+ ┌────────────────────────────────────────▼──────────────────────────────────────────────────┐
139
+ │ SYSTEM 1: Jev Decision Plane (<5ms) │
140
+ │ • Tool Call Validation • Repeated Loop Detection • Test Result Triage │
141
+ │ • Surgical Diff Enforcement • Context Noise Filtering │
142
+ └────────────────────────────────────────┬──────────────────────────────────────────────────┘
143
+ │
144
+ ┌───────────────────┴───────────────────┐
145
+ ▼ ▼
146
+ ┌───────────────────────────┐ ┌───────────────────────────┐
147
+ │ Online Cloud Backend │ │ Offline Local Engine │
148
+ │ (169Pi / Claude / GPT) │ │ (llama.cpp Qwen GGUF) │
149
+ │ • Sub-12s Task Completion│ │ • Zero Context Overflow │
150
+ │ • >75% Token Savings │ │ • Zero Syntax Crash Loops│
151
+ └───────────────────────────┘ └───────────────────────────┘
152
+ ```
153
+
154
+ ### Key Capabilities
155
+
156
+ 1. **⚡ TypeSafe System 1 Decision Loop (`src/codeagent/system1.py`)**:
157
+ - **Loop & Oscillation Detection**: Tracks tool call signatures. If the agent fails an operation twice (e.g. repeated syntax errors), System 1 intercepts the turn and forces a targeted `edit_file` directive rather than an infinite full-file rewrite.
158
+ - **Automated Test Health Parser**: Fast regex engine parsing pytest, unittest, and npm test outputs in <1ms to determine test success without burning LLM inference.
159
+ - **Tool Parameter Validator**: Pre-execution validation preventing malformed or missing parameters.
160
+
161
+ 2. **🚀 Multi-Tier Caching System (`src/codeagent/cache.py`)**:
162
+ - **`FileReadCache`**: In-memory cache for repeated file reads keyed by `(path, mtime, size, line_range)`. Cuts read latency from 1.2ms to **<0.05ms** (24x speedup).
163
+ - **`TestResultCache`**: Disk-persisted cache in `~/.alpiecode/cache/test_cache.json` keyed by a workspace file fingerprint (`hash(mtimes)`). Unchanged test suites re-run in **<1ms** with zero redundant execution.
164
+ - **`ResponseCache`**: Disk-backed LRU cache for pure text explanations and Q&A.
165
+
166
+ 3. **🗜️ Context Noise Filtering (`src/codeagent/context.py`)**:
167
+ - Compresses 50+ lines of clean passing test noise down to a 2-line summary (`[Clean Test Output]`).
168
+ - On test failures, preserves critical tracebacks and failure frames while stripping passing test dots, achieving **>75% token reduction**.
169
+
170
+ 4. **📊 Real-Time Performance Observability (`src/codeagent/metrics.py`)**:
171
+ - At the conclusion of every task, AlpieCode renders an end-of-task summary:
172
+ ```text
173
+ ╭──────────────────────── 📊 Task Performance Summary ────────────────────────╮
174
+ │ ⏱️ Latency: 12.22s | 🧠 LLM Calls: 2 (5425ms) │
175
+ │ 🛠️ Tools Time: 153.5ms (1 calls) | 📦 Context Prep: 6253ms | ⚡ Cache: 1│
176
+ ╰─────────────────────────────────────────────────────────────────────────────╯
177
+ ```
178
+
179
+ 5. **📜 Agent Operational Guidelines (`agents.md`)**:
180
+ - Operational rules defining the **Investigate → Plan → Surgical Edit → Verify → Complete** cycle, enforcing surgical diffs for files >100 lines and zero stub implementations.
181
+
182
+ ---
183
+
184
+ ## 📊 Performance & Optimization Benchmarks
185
+
186
+ Comprehensive benchmarks comparing AlpieCode before vs. after the System 1 Jev engine and caching upgrade:
187
+
188
+ | Benchmark Metric | Baseline (v9.0) | With System 1 & Caching (v10.0.2) | Improvement |
189
+ | :--- | :---: | :---: | :---: |
190
+ | **Simple Test & Verify Task Latency** | 45.57s | **12.22s** | **3.7x faster (73% drop)** |
191
+ | **Repeated File Read Latency** | 1.20 ms | **<0.05 ms** | **24x faster** (In-Memory Cache) |
192
+ | **Unchanged Test Suite Re-run** | 3,500 ms | **<1.0 ms** | **Instant (Zero-Latency Cache)** |
193
+ | **Test Output Context Token Footprint**| ~3,500 tokens (500 lines) | **~80 tokens (4 lines)** | **>75% Context Compression** |
194
+ | **Syntax Error Retry Recovery** | Infinite full-file loop | **Loop intercepted on Turn 2** | Self-healing surgical diff |
195
+ | **Full Regression Test Suite** | 52 passed | **96 passed (100% green)** | Complete reliability |
196
+
197
+ ---
198
+
125
199
  ## 📦 Complete Installation Guide
126
200
 
127
201
  ### 1. Prerequisites
@@ -177,7 +251,7 @@ pip install dist/alpiecode-9.0.1-py3-none-any.whl
177
251
  #### Verify CLI Installation:
178
252
  ```bash
179
253
  alpiecode version
180
- # Output: alpiecode 9.0.1
254
+ # Output: alpiecode 10.0.2
181
255
 
182
256
  alpiecode doctor
183
257
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpiecode"
7
- version = "10.0.2"
7
+ version = "10.0.3"
8
8
  description = "AlpieCode"
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: 10.0.2
3
+ Version: 10.0.3
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -48,8 +48,11 @@ test/test_cache_reads.py
48
48
  test/test_chat_mode.py
49
49
  test/test_claude_codex_capabilities.py
50
50
  test/test_context_filter.py
51
+ test/test_math_tools.py
51
52
  test/test_metrics.py
52
53
  test/test_offline_mode.py
54
+ test/test_offline_opt.py
55
+ test/test_palindrome_check.py
53
56
  test/test_phase1.py
54
57
  test/test_phase2.py
55
58
  test/test_phase3.py
@@ -17,7 +17,7 @@ Supports:
17
17
  from pathlib import Path
18
18
  from typing import Optional, Any, Dict
19
19
 
20
- __version__ = "10.0.2"
20
+ __version__ = "10.0.3"
21
21
 
22
22
  def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
23
23
  """Run an autonomous coding task programmatically."""
@@ -202,6 +202,9 @@ class LocalBackend:
202
202
  full_content = []
203
203
  full_reasoning = []
204
204
  tool_calls_dict = {}
205
+ # In non-thinking mode, swallow any accidental <think>...</think> generated by model
206
+ suppress_think = not enable_thinking
207
+ inside_suppressed_think = False
205
208
 
206
209
  for chunk in chunks:
207
210
  if not chunk or not chunk.get("choices"):
@@ -210,7 +213,7 @@ class LocalBackend:
210
213
  delta = choice.get("delta") or {}
211
214
 
212
215
  explicit_reasoning = delta.get("reasoning") or delta.get("reasoning_content")
213
- if explicit_reasoning:
216
+ if explicit_reasoning and enable_thinking:
214
217
  if not think_started:
215
218
  think_started = True
216
219
  yield ("thinking_start", {"time": start_time})
@@ -238,6 +241,23 @@ class LocalBackend:
238
241
 
239
242
  text = delta.get("content")
240
243
  if text:
244
+ if suppress_think:
245
+ # Silently swallow any think tags/blocks emitted in fast mode
246
+ if "<think>" in text:
247
+ inside_suppressed_think = True
248
+ text = text.replace("<think>", "")
249
+ if inside_suppressed_think:
250
+ if "</think>" in text:
251
+ _, after = text.split("</think>", 1)
252
+ inside_suppressed_think = False
253
+ text = after
254
+ else:
255
+ text = ""
256
+ else:
257
+ text = text.replace("</think>", "")
258
+ if not text:
259
+ continue
260
+
241
261
  if in_think:
242
262
  if not think_started:
243
263
  think_started = True
@@ -286,6 +306,10 @@ class LocalBackend:
286
306
  tool_calls.append(ToolCall(id=item["id"], name=item["name"], arguments=args))
287
307
 
288
308
  final_content_str = "".join(full_content).strip()
309
+ if not enable_thinking and final_content_str:
310
+ import re
311
+ final_content_str = re.sub(r"^\s*<think>.*?</think>\s*", "", final_content_str, flags=re.DOTALL).strip()
312
+ final_content_str = re.sub(r"^\s*(?:</think>|<think>)*\s*", "", final_content_str).strip()
289
313
  final_reasoning_str = "".join(full_reasoning).strip()
290
314
 
291
315
  if not final_content_str and final_reasoning_str:
@@ -246,15 +246,32 @@ class FileReadCache:
246
246
 
247
247
 
248
248
  class TestResultCache:
249
- """Caches test command execution results when workspace files are unchanged."""
249
+ """Caches test command execution results when workspace files are unchanged.
250
+ Persists across CLI runs in ~/.alpiecode/cache/test_cache.json
251
+ """
250
252
  __test__ = False
251
253
 
252
254
  def __init__(self, max_entries: int = 100):
253
255
  self.max_entries = max_entries
256
+ self.disk_file = CACHE_DIR / "test_cache.json"
254
257
  self._cache: Dict[str, Dict[str, Any]] = {}
255
- self._workspace_versions: Dict[str, float] = {}
256
258
  self.hits = 0
257
259
  self.misses = 0
260
+ self._load_disk()
261
+
262
+ def _load_disk(self):
263
+ if self.disk_file.exists():
264
+ try:
265
+ self._cache = json.loads(self.disk_file.read_text(encoding="utf-8"))
266
+ except Exception:
267
+ self._cache = {}
268
+
269
+ def _save_disk(self):
270
+ try:
271
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
272
+ self.disk_file.write_text(json.dumps(self._cache, indent=2), encoding="utf-8")
273
+ except Exception:
274
+ pass
258
275
 
259
276
  def _is_test_cmd(self, cmd: str) -> bool:
260
277
  c = cmd.strip().lower()
@@ -266,10 +283,21 @@ class TestResultCache:
266
283
  or "go test" in c
267
284
  )
268
285
 
286
+ def _get_workspace_fingerprint(self, workdir: Path) -> str:
287
+ try:
288
+ mtimes = []
289
+ for ext in ("*.py", "*.js", "*.ts", "*.json"):
290
+ for p in workdir.glob(ext):
291
+ if p.is_file() and not p.name.startswith("."):
292
+ mtimes.append(f"{p.name}:{p.stat().st_mtime_ns}")
293
+ return hashlib.md5("".join(sorted(mtimes)).encode()).hexdigest()[:12]
294
+ except Exception:
295
+ return "0"
296
+
269
297
  def get(self, workdir: Path, cmd: str) -> Optional[str]:
270
298
  if not self._is_test_cmd(cmd):
271
299
  return None
272
- version = self._workspace_versions.get(str(workdir.resolve()), 0.0)
300
+ version = self._get_workspace_fingerprint(workdir)
273
301
  key = f"{workdir.resolve()}:{cmd.strip()}:{version}"
274
302
  entry = self._cache.get(key)
275
303
  if entry is not None:
@@ -281,17 +309,20 @@ class TestResultCache:
281
309
  def put(self, workdir: Path, cmd: str, result: str) -> None:
282
310
  if not self._is_test_cmd(cmd):
283
311
  return
284
- version = self._workspace_versions.get(str(workdir.resolve()), 0.0)
312
+ version = self._get_workspace_fingerprint(workdir)
285
313
  key = f"{workdir.resolve()}:{cmd.strip()}:{version}"
286
314
  self._cache[key] = {"content": result, "cached_at": time.time()}
315
+ self._save_disk()
287
316
 
288
317
  def invalidate(self, workdir: Optional[Path] = None) -> None:
289
318
  if workdir is None:
290
319
  self._cache.clear()
291
- self._workspace_versions.clear()
292
- return
293
- w_key = str(workdir.resolve())
294
- self._workspace_versions[w_key] = time.time()
320
+ else:
321
+ prefix = f"{workdir.resolve()}:"
322
+ keys_to_del = [k for k in self._cache if k.startswith(prefix)]
323
+ for k in keys_to_del:
324
+ self._cache.pop(k, None)
325
+ self._save_disk()
295
326
 
296
327
 
297
328
  _FILE_CACHE: Optional[FileReadCache] = None
@@ -87,6 +87,7 @@ def main():
87
87
  common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
88
88
  common.add_argument("--debug", action="store_true", help="Show autonomous discovery and debug diagnostics")
89
89
  common.add_argument("--offline", "--local", dest="offline", action="store_true", help="Run using local offline GGUF model on GPU without internet")
90
+ common.add_argument("--ctx-size", type=int, default=None, help="Context size in tokens (default: 4096 offline, 262144 online)")
90
91
 
91
92
  from . import __version__
92
93
  parser = argparse.ArgumentParser(
@@ -174,10 +175,14 @@ def main():
174
175
 
175
176
  if getattr(args, "offline", False):
176
177
  cfg.base_url = "offline"
177
- cfg.n_ctx = 16384
178
+ # 4096 default fits 4.4GB model + KV cache 100% in 6GB VRAM with zero host-paging
178
179
  import os
180
+ cfg.n_ctx = int(os.environ.get("ALPIECODE_CTX_SIZE", 4096))
179
181
  os.environ["ALPIECODE_BASE_URL"] = "offline"
180
182
 
183
+ if getattr(args, "ctx_size", None):
184
+ cfg.n_ctx = args.ctx_size
185
+
181
186
  if getattr(args, "thinking", False):
182
187
  cfg.enable_thinking = True
183
188
  elif getattr(args, "no_thinking", False):
@@ -237,20 +237,41 @@ def _ensure_llama_cpp():
237
237
 
238
238
  is_win = sys.platform == "win32"
239
239
  is_mac = sys.platform == "darwin"
240
+ # Check if NVIDIA GPU is present on Linux
241
+ has_nvidia = False
242
+ if not is_win and not is_mac:
243
+ for smi in ["nvidia-smi", "/usr/lib/wsl/lib/nvidia-smi"]:
244
+ try:
245
+ if subprocess.run([smi], capture_output=True, timeout=2).returncode == 0:
246
+ has_nvidia = True
247
+ break
248
+ except Exception:
249
+ pass
250
+
240
251
  if is_win:
241
252
  wheel_url = "https://github.com/abetlen/llama-cpp-python/releases/download/v0.3.34-vulkan/llama_cpp_python-0.3.34-py3-none-win_amd64.whl"
253
+ install_targets = [wheel_url, "llama-cpp-python"]
242
254
  elif is_mac:
243
255
  wheel_url = "https://github.com/abetlen/llama-cpp-python/releases/download/v0.3.34-metal/llama_cpp_python-0.3.34-py3-none-macosx_11_0_arm64.whl"
256
+ install_targets = [wheel_url, "llama-cpp-python"]
257
+ elif has_nvidia:
258
+ # Linux with NVIDIA: use pre-compiled CUDA 12.4 / 12.1 wheel index
259
+ wheel_url = "https://abetlen.github.io/llama-cpp-python/whl/cu124"
260
+ install_targets = [
261
+ ["--extra-index-url", wheel_url, "llama-cpp-python"],
262
+ ["--extra-index-url", "https://abetlen.github.io/llama-cpp-python/whl/cu121", "llama-cpp-python"],
263
+ "llama-cpp-python",
264
+ ]
244
265
  else:
245
266
  wheel_url = "https://github.com/abetlen/llama-cpp-python/releases/download/v0.3.34-vulkan/llama_cpp_python-0.3.34-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"
246
-
247
- install_targets = [wheel_url, "llama-cpp-python"]
267
+ install_targets = [wheel_url, "llama-cpp-python"]
248
268
  installed = False
249
269
  last_error = ""
250
270
  for target in install_targets:
271
+ target_args = target if isinstance(target, list) else [target]
251
272
  for cmd_base in [
252
- ["uv", "pip", "install", target],
253
- [sys.executable, "-m", "pip", "install", target],
273
+ ["uv", "pip", "install"] + target_args,
274
+ [sys.executable, "-m", "pip", "install"] + target_args,
254
275
  ]:
255
276
  try:
256
277
  res = subprocess.run(cmd_base, capture_output=True, text=True, timeout=180)
@@ -421,7 +442,18 @@ class LocalModel:
421
442
  enable_thinking = kwargs.get("enable_thinking", True)
422
443
 
423
444
  if not enable_thinking:
424
- msgs = list(messages) + [{"role": "assistant", "content": "<think>\n\n</think>\n\n"}]
445
+ # Instruct model cleanly in prompt rather than injecting closed assistant turn
446
+ msgs = []
447
+ directive_tag = "[Directive: Respond directly with code or tool calls. Do not output <think> tags.]"
448
+ for i, m in enumerate(messages):
449
+ if i == len(messages) - 1 and m.get("role") == "user":
450
+ content = m.get("content", "")
451
+ if isinstance(content, str) and directive_tag not in content:
452
+ msgs.append({**m, "content": f"{content}\n\n{directive_tag}"})
453
+ else:
454
+ msgs.append(m)
455
+ else:
456
+ msgs.append(m)
425
457
  else:
426
458
  msgs = messages
427
459
 
@@ -469,13 +501,13 @@ class LocalModel:
469
501
  else:
470
502
  raise
471
503
 
472
- # Fix prefill response content so chat history remains valid across multi-turn sessions
504
+ # Clean any accidental think tags from non-thinking responses
473
505
  if not enable_thinking and result_dict.get("choices"):
474
506
  msg_dict = result_dict["choices"][0].get("message", {})
475
507
  raw_content = msg_dict.get("content") or ""
476
- # Strip redundant opening/closing think tags output by model after prefill
477
- cleaned = re.sub(r"^\s*(?:</think>|<think>)*\s*", "", raw_content)
478
- msg_dict["content"] = f"<think>\n\n</think>\n\n{cleaned}"
508
+ cleaned = re.sub(r"^\s*<think>.*?</think>\s*", "", raw_content, flags=re.DOTALL)
509
+ cleaned = re.sub(r"^\s*(?:</think>|<think>)*\s*", "", cleaned)
510
+ msg_dict["content"] = cleaned
479
511
 
480
512
  return _DictWrapper(result_dict)
481
513
 
@@ -105,7 +105,11 @@ def save_memory(workdir: Path, content: str, memory_type: str = "learning") -> N
105
105
  save_memories_batch(workdir, [{"content": content, "type": memory_type}])
106
106
 
107
107
 
108
- def format_memories_for_prompt(workdir: Path) -> Optional[str]:
108
+ def format_memories_for_prompt(
109
+ workdir: Path,
110
+ max_entries: int = 5,
111
+ max_chars: int = 600,
112
+ ) -> Optional[str]:
109
113
  """
110
114
  Format memories into a string suitable for injection into the system prompt.
111
115
 
@@ -116,9 +120,24 @@ def format_memories_for_prompt(workdir: Path) -> Optional[str]:
116
120
  if not memories:
117
121
  return None
118
122
 
119
- lines = ["## Recalled memories from previous sessions on this project:\n"]
120
- for mem in memories[-10:]: # Only inject last 10 to save context
121
- lines.append(f"- [{mem.get('type', 'note')}] {mem['content']}")
123
+ lines = ["## Recalled memories from previous sessions on this project:"]
124
+ total_chars = 0
125
+ # Use most recent memories first, single-line summarized to keep prompt lean
126
+ for mem in reversed(memories):
127
+ c = mem.get("content", "").strip()
128
+ if not c:
129
+ continue
130
+ first_line = c.split("\n")[0][:120]
131
+ entry = f"- [{mem.get('type', 'note')}] {first_line}"
132
+ if total_chars + len(entry) > max_chars:
133
+ break
134
+ lines.append(entry)
135
+ total_chars += len(entry)
136
+ if len(lines) - 1 >= max_entries:
137
+ break
138
+
139
+ if len(lines) <= 1:
140
+ return None
122
141
 
123
142
  lines.append(
124
143
  "\nNote: These memories are from previous sessions. "
@@ -27,6 +27,16 @@ class TaskMetrics:
27
27
  tool_call_count: int = 0
28
28
  cache_hits: int = 0
29
29
  errors_encountered: int = 0
30
+ _initial_file_hits: int = 0
31
+ _initial_test_hits: int = 0
32
+
33
+ def __post_init__(self):
34
+ try:
35
+ from .cache import get_file_cache, get_test_cache
36
+ self._initial_file_hits = get_file_cache().hits
37
+ self._initial_test_hits = get_test_cache().hits
38
+ except Exception:
39
+ pass
30
40
 
31
41
  def start(self) -> "TaskMetrics":
32
42
  self.start_time = time.time()
@@ -59,11 +69,19 @@ class TaskMetrics:
59
69
  if self.end_time is None:
60
70
  latency_ms = (time.time() - self.start_time) * 1000.0
61
71
 
72
+ total_cache_hits = self.cache_hits
73
+ try:
74
+ from .cache import get_file_cache, get_test_cache
75
+ delta = (get_file_cache().hits - self._initial_file_hits) + (get_test_cache().hits - self._initial_test_hits)
76
+ total_cache_hits += max(0, delta)
77
+ except Exception:
78
+ pass
79
+
62
80
  return {
63
81
  "task_id": self.task_id,
64
82
  "llm_call_count": self.llm_call_count,
65
83
  "tool_call_count": self.tool_call_count,
66
- "cache_hits": self.cache_hits,
84
+ "cache_hits": total_cache_hits,
67
85
  "total_task_latency_ms": round(latency_ms, 2),
68
86
  "total_task_latency_s": round(latency_ms / 1000.0, 2),
69
87
  "total_tool_execution_ms": round(self.total_tool_execution_ms, 2),
@@ -211,6 +211,7 @@ class AgentOrchestrator:
211
211
  is_offline = not task_backend.is_available or isinstance(task_backend, LocalBackend)
212
212
  session.is_offline = is_offline
213
213
 
214
+ ctx_prep_start = time.time()
214
215
  # ── Configure tools & system prompt based on complexity ──
215
216
  active_tools = self.prompt_builder.get_tools(is_offline=is_offline, complexity=complexity, task_context=task_context)
216
217
  system_prompt = self.prompt_builder.build_system_prompt(
@@ -279,6 +280,7 @@ class AgentOrchestrator:
279
280
  else:
280
281
  base_enable_thinking = task_context.enable_thinking
281
282
 
283
+ task_metrics.record_context_prep((time.time() - ctx_prep_start) * 1000.0)
282
284
  # ── Goal-driven turn loop (no fixed limit) ──
283
285
  progress_monitor = ProgressMonitor()
284
286
  turn = 0
@@ -427,6 +429,8 @@ class AgentOrchestrator:
427
429
  yield AgentEvent("error", {"error": "Backend returned no response"})
428
430
  return
429
431
 
432
+ task_metrics.record_llm_call((time.time() - llm_call_start) * 1000.0)
433
+
430
434
  if resp.reasoning and not hasattr(task_backend, "chat_completion_stream"):
431
435
  yield AgentEvent("thinking", {"content": resp.reasoning})
432
436
 
@@ -446,6 +450,11 @@ class AgentOrchestrator:
446
450
  resp.content = resp.reasoning.strip()
447
451
  resp.reasoning = None
448
452
 
453
+ # Clean any stray </think> tags from content
454
+ if resp.content and "</think>" in resp.content:
455
+ parts = resp.content.split("</think>", 1)
456
+ resp.content = parts[1].strip()
457
+
449
458
  # ── DONE detection in assistant content ──
450
459
  if resp.content and "DONE:" in resp.content.upper():
451
460
  # Model said DONE — finish even if there are tool calls
@@ -549,7 +549,11 @@ class PromptBuilder:
549
549
  prompt += "\n\n" + GOAL_DRIVEN_RULES
550
550
  # qa intent: no extra prompt needed (model answers directly)
551
551
 
552
- memories = format_memories_for_prompt(workdir)
552
+ # For offline mode, keep system prompt lean to ensure sub-10s prefill on CPU/GPU
553
+ if is_offline:
554
+ memories = format_memories_for_prompt(workdir, max_entries=2, max_chars=250)
555
+ else:
556
+ memories = format_memories_for_prompt(workdir, max_entries=5, max_chars=800)
553
557
  if memories:
554
558
  prompt += f"\n\n{memories}"
555
559
  return prompt
@@ -551,13 +551,27 @@ def _read_file(workdir: Path, path: str, start_line: int = None, end_line: int =
551
551
  return result
552
552
 
553
553
 
554
+ def _resolve_safe_path(workdir: Path, path: str) -> Path:
555
+ """Resolve a path safely within workdir, stripping synthetic root prefixes."""
556
+ raw_p = Path(path)
557
+ if raw_p.is_absolute():
558
+ try:
559
+ raw_p.resolve().relative_to(workdir.resolve())
560
+ return raw_p
561
+ except (ValueError, RuntimeError):
562
+ parts = [part for part in raw_p.parts if part not in ("/", "\\", "home", "user", "project", "workspace", "app", "root")]
563
+ if parts:
564
+ return workdir / Path(*parts)
565
+ return workdir / raw_p.name
566
+ return workdir / raw_p
567
+
554
568
  def _write_file(workdir: Path, path: str, content: str) -> str:
555
569
  # Pre-commit AST syntax check to prevent writing corrupt code
556
570
  is_valid, err = validate_code_syntax(path, content)
557
571
  if not is_valid:
558
572
  return f"error: Pre-commit syntax check failed for '{path}':\n{err}\nPlease fix the syntax error before writing."
559
573
 
560
- p = workdir / path
574
+ p = _resolve_safe_path(workdir, path)
561
575
  p.parent.mkdir(parents=True, exist_ok=True)
562
576
  p.write_text(content, encoding="utf-8")
563
577
  _RECENTLY_WRITTEN_FILES.add(Path(path).name.lower())
@@ -567,7 +581,7 @@ def _write_file(workdir: Path, path: str, content: str) -> str:
567
581
 
568
582
 
569
583
  def _edit_file(workdir: Path, path: str, old_str: str, new_str: str) -> str:
570
- p = workdir / path
584
+ p = _resolve_safe_path(workdir, path)
571
585
  if not p.exists():
572
586
  return f"error: file not found: {path}"
573
587
  text = p.read_text(encoding="utf-8", errors="replace")
@@ -0,0 +1,214 @@
1
+ """
2
+ Unit tests for math_tools module.
3
+
4
+ Run tests using: python3 -m unittest test_math_tools
5
+ """
6
+
7
+ import unittest
8
+ from math_tools import add, multiply
9
+
10
+
11
+ class TestAddFunction(unittest.TestCase):
12
+ """Test cases for the add function."""
13
+
14
+ def test_positive_integers(self):
15
+ """Test addition with positive integers."""
16
+ self.assertEqual(add(2, 3), 5)
17
+ self.assertEqual(add(10, 20), 30)
18
+ self.assertEqual(add(100, 200), 300)
19
+
20
+ def test_negative_integers(self):
21
+ """Test addition with negative integers."""
22
+ self.assertEqual(add(-2, -3), -5)
23
+ self.assertEqual(add(-10, -20), -30)
24
+ self.assertEqual(add(-100, -200), -300)
25
+
26
+ def test_mixed_positive_negative(self):
27
+ """Test addition with mixed positive and negative integers."""
28
+ self.assertEqual(add(-2, 3), 1)
29
+ self.assertEqual(add(2, -3), -1)
30
+ self.assertEqual(add(-10, 20), 10)
31
+ self.assertEqual(add(10, -20), -10)
32
+
33
+ def test_zero_inputs(self):
34
+ """Test addition with zero."""
35
+ self.assertEqual(add(0, 0), 0)
36
+ self.assertEqual(add(5, 0), 5)
37
+ self.assertEqual(add(0, 5), 5)
38
+ self.assertEqual(add(-5, 0), -5)
39
+ self.assertEqual(add(0, -5), -5)
40
+
41
+ def test_float_inputs(self):
42
+ """Test addition with floating point numbers."""
43
+ self.assertAlmostEqual(add(1.5, 2.5), 4.0)
44
+ self.assertAlmostEqual(add(0.1, 0.2), 0.3, places=10)
45
+ self.assertAlmostEqual(add(-1.5, 2.5), 1.0)
46
+ self.assertAlmostEqual(add(1.234, 5.678), 6.912, places=10)
47
+
48
+ def test_large_numbers(self):
49
+ """Test addition with large numbers."""
50
+ self.assertEqual(add(10**100, 10**100), 2 * 10**100)
51
+ self.assertEqual(add(-10**100, 10**100), 0)
52
+ self.assertEqual(add(10**100, -10**100 + 1), 1)
53
+
54
+ def test_small_numbers(self):
55
+ """Test addition with very small numbers."""
56
+ self.assertAlmostEqual(add(1e-10, 2e-10), 3e-10, places=10)
57
+ self.assertAlmostEqual(add(-1e-10, 1e-10), 0.0, places=10)
58
+
59
+ def test_invalid_string_input(self):
60
+ """Test that string inputs raise TypeError."""
61
+ with self.assertRaises(TypeError):
62
+ add("5", 3)
63
+ with self.assertRaises(TypeError):
64
+ add(5, "3")
65
+ with self.assertRaises(TypeError):
66
+ add("hello", "world")
67
+
68
+ def test_invalid_list_input(self):
69
+ """Test that list inputs raise TypeError."""
70
+ with self.assertRaises(TypeError):
71
+ add([1, 2], 3)
72
+ with self.assertRaises(TypeError):
73
+ add(5, [1, 2])
74
+
75
+ def test_invalid_dict_input(self):
76
+ """Test that dict inputs raise TypeError."""
77
+ with self.assertRaises(TypeError):
78
+ add({"a": 1}, 3)
79
+ with self.assertRaises(TypeError):
80
+ add(5, {"b": 2})
81
+
82
+ def test_invalid_none_input(self):
83
+ """Test that None inputs raise TypeError."""
84
+ with self.assertRaises(TypeError):
85
+ add(None, 3)
86
+ with self.assertRaises(TypeError):
87
+ add(5, None)
88
+
89
+ def test_invalid_bool_input(self):
90
+ """Test that bool inputs raise TypeError (bool is subclass of int)."""
91
+ with self.assertRaises(TypeError):
92
+ add(True, 3)
93
+ with self.assertRaises(TypeError):
94
+ add(5, False)
95
+ with self.assertRaises(TypeError):
96
+ add(True, False)
97
+
98
+ def test_complex_numbers(self):
99
+ """Test that complex numbers raise TypeError."""
100
+ with self.assertRaises(TypeError):
101
+ add(1 + 2j, 3)
102
+ with self.assertRaises(TypeError):
103
+ add(5, 1 + 2j)
104
+
105
+
106
+ class TestMultiplyFunction(unittest.TestCase):
107
+ """Test cases for the multiply function."""
108
+
109
+ def test_positive_integers(self):
110
+ """Test multiplication with positive integers."""
111
+ self.assertEqual(multiply(2, 3), 6)
112
+ self.assertEqual(multiply(10, 20), 200)
113
+ self.assertEqual(multiply(7, 8), 56)
114
+
115
+ def test_negative_integers(self):
116
+ """Test multiplication with negative integers."""
117
+ self.assertEqual(multiply(-2, -3), 6)
118
+ self.assertEqual(multiply(-10, -20), 200)
119
+ self.assertEqual(multiply(-7, 8), -56)
120
+ self.assertEqual(multiply(7, -8), -56)
121
+
122
+ def test_zero_inputs(self):
123
+ """Test multiplication with zero."""
124
+ self.assertEqual(multiply(0, 0), 0)
125
+ self.assertEqual(multiply(5, 0), 0)
126
+ self.assertEqual(multiply(0, 5), 0)
127
+ self.assertEqual(multiply(-5, 0), 0)
128
+ self.assertEqual(multiply(0, -5), 0)
129
+
130
+ def test_float_inputs(self):
131
+ """Test multiplication with floating point numbers."""
132
+ self.assertAlmostEqual(multiply(1.5, 2.5), 3.75)
133
+ self.assertAlmostEqual(multiply(0.1, 0.2), 0.02, places=10)
134
+ self.assertAlmostEqual(multiply(-1.5, 2.5), -3.75)
135
+ self.assertAlmostEqual(multiply(2.5, 0.4), 1.0)
136
+
137
+ def test_large_numbers(self):
138
+ """Test multiplication with large numbers."""
139
+ self.assertEqual(multiply(10**50, 10**50), 10**100)
140
+ self.assertEqual(multiply(-10**50, 10**50), -10**100)
141
+ self.assertEqual(multiply(10**100, 10**100), 10**200)
142
+
143
+ def test_small_numbers(self):
144
+ """Test multiplication with very small numbers."""
145
+ self.assertAlmostEqual(multiply(1e-10, 2e-10), 2e-20, places=10)
146
+ self.assertAlmostEqual(multiply(-1e-10, 1e-10), -1e-20, places=10)
147
+
148
+ def test_invalid_string_input(self):
149
+ """Test that string inputs raise TypeError."""
150
+ with self.assertRaises(TypeError):
151
+ multiply("5", 3)
152
+ with self.assertRaises(TypeError):
153
+ multiply(5, "3")
154
+ with self.assertRaises(TypeError):
155
+ multiply("hello", "world")
156
+
157
+ def test_invalid_list_input(self):
158
+ """Test that list inputs raise TypeError."""
159
+ with self.assertRaises(TypeError):
160
+ multiply([1, 2], 3)
161
+ with self.assertRaises(TypeError):
162
+ multiply(5, [1, 2])
163
+
164
+ def test_invalid_dict_input(self):
165
+ """Test that dict inputs raise TypeError."""
166
+ with self.assertRaises(TypeError):
167
+ multiply({"a": 1}, 3)
168
+ with self.assertRaises(TypeError):
169
+ multiply(5, {"b": 2})
170
+
171
+ def test_invalid_none_input(self):
172
+ """Test that None inputs raise TypeError."""
173
+ with self.assertRaises(TypeError):
174
+ multiply(None, 3)
175
+ with self.assertRaises(TypeError):
176
+ multiply(5, None)
177
+
178
+ def test_invalid_bool_input(self):
179
+ """Test that bool inputs raise TypeError (bool is subclass of int)."""
180
+ with self.assertRaises(TypeError):
181
+ multiply(True, 3)
182
+ with self.assertRaises(TypeError):
183
+ multiply(5, False)
184
+ with self.assertRaises(TypeError):
185
+ multiply(True, False)
186
+
187
+ def test_complex_numbers(self):
188
+ """Test that complex numbers raise TypeError."""
189
+ with self.assertRaises(TypeError):
190
+ multiply(1 + 2j, 3)
191
+ with self.assertRaises(TypeError):
192
+ multiply(5, 1 + 2j)
193
+
194
+
195
+ class TestModuleImport(unittest.TestCase):
196
+ """Test that the module can be imported correctly."""
197
+
198
+ def test_module_import(self):
199
+ """Test that both functions can be imported from the module."""
200
+ from math_tools import add, multiply
201
+ self.assertTrue(callable(add))
202
+ self.assertTrue(callable(multiply))
203
+
204
+ def test_function_names(self):
205
+ """Test that function names are correct."""
206
+ import math_tools
207
+ self.assertTrue(hasattr(math_tools, 'add'))
208
+ self.assertTrue(hasattr(math_tools, 'multiply'))
209
+ self.assertTrue(callable(getattr(math_tools, 'add')))
210
+ self.assertTrue(callable(getattr(math_tools, 'multiply')))
211
+
212
+
213
+ if __name__ == '__main__':
214
+ unittest.main()
@@ -0,0 +1,59 @@
1
+ import unittest
2
+ from pathlib import Path
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ from codeagent.memory import format_memories_for_prompt
6
+ from codeagent.prompt import PromptBuilder
7
+
8
+
9
+ class TestOfflineOptimization(unittest.TestCase):
10
+ def test_offline_prompt_is_lean(self):
11
+ pb = PromptBuilder()
12
+ prompt = pb.build_system_prompt(Path("."), is_offline=True, complexity="low")
13
+ # Ensure offline prompt is lean and under 2500 characters
14
+ self.assertLess(len(prompt), 2500)
15
+ self.assertIn("OFFLINE mode", prompt)
16
+
17
+ def test_memory_character_and_entry_capping(self):
18
+ fake_memories = [
19
+ {"content": "line 1\nline 2\nline 3" + "x" * 500, "type": "command"},
20
+ {"content": "def very_long_function():\n pass\n" + "y" * 500, "type": "structure"},
21
+ {"content": "third memory\n" + "z" * 500, "type": "learning"},
22
+ ]
23
+ with patch("codeagent.memory.load_memories", return_value=fake_memories):
24
+ formatted = format_memories_for_prompt(Path("."), max_entries=2, max_chars=200)
25
+ self.assertIsNotNone(formatted)
26
+ self.assertLess(len(formatted), 400)
27
+ # Ensure multi-line scripts are condensed to single-line summaries
28
+ self.assertNotIn("def very_long_function():\n pass", formatted)
29
+
30
+ def test_stream_think_suppression_logic(self):
31
+ from codeagent.backends.local_backend import LocalBackend
32
+ from codeagent.config import Config
33
+
34
+ cfg = Config(base_url="offline", n_ctx=4096)
35
+ backend = LocalBackend(cfg)
36
+
37
+ # Mock chunk generator from model emitting think tags in non-thinking mode
38
+ mock_chunks = [
39
+ {"choices": [{"delta": {"content": "<think>\nThinking step 1...\n</think>"}}]},
40
+ {"choices": [{"delta": {"content": "Here is the code."}}]},
41
+ ]
42
+ mock_model = MagicMock()
43
+ mock_model.create_chat_completion.return_value = mock_chunks
44
+ backend._model = mock_model
45
+
46
+ events = list(backend.chat_completion_stream(
47
+ messages=[{"role": "user", "content": "test"}],
48
+ enable_thinking=False,
49
+ ))
50
+
51
+ tokens = [data["delta"] for event_type, data in events if event_type == "token"]
52
+ combined_tokens = "".join(tokens)
53
+ self.assertNotIn("<think>", combined_tokens)
54
+ self.assertNotIn("Thinking step 1", combined_tokens)
55
+ self.assertIn("Here is the code.", combined_tokens)
56
+
57
+
58
+ if __name__ == "__main__":
59
+ unittest.main()
@@ -0,0 +1,50 @@
1
+ import unittest
2
+ from palindrome_check import is_palindrome, is_palindrome_strict
3
+
4
+
5
+ class TestPalindromeCheck(unittest.TestCase):
6
+
7
+ def test_palindrome_basic(self):
8
+ self.assertTrue(is_palindrome("racecar"))
9
+ self.assertTrue(is_palindrome("Racecar"))
10
+ self.assertTrue(is_palindrome("a"))
11
+ self.assertTrue(is_palindrome(""))
12
+
13
+ def test_palindrome_case_insensitive(self):
14
+ self.assertTrue(is_palindrome("A man a plan a canal Panama"))
15
+ self.assertTrue(is_palindrome("Was it a car or a cat I saw"))
16
+
17
+ def test_palindrome_numbers(self):
18
+ self.assertTrue(is_palindrome("12321"))
19
+ self.assertTrue(is_palindrome("121"))
20
+
21
+ def test_not_palindrome(self):
22
+ self.assertFalse(is_palindrome("hello"))
23
+ self.assertFalse(is_palindrome("hello, world"))
24
+ self.assertFalse(is_palindrome("12345"))
25
+
26
+ def test_palindrome_strict_case_sensitive(self):
27
+ self.assertTrue(is_palindrome_strict("noon"))
28
+ self.assertTrue(is_palindrome_strict("level"))
29
+ self.assertTrue(is_palindrome_strict("racecar"))
30
+ self.assertFalse(is_palindrome_strict("Racecar")) # Case-sensitive: "Racecar" != "racecar"
31
+ self.assertFalse(is_palindrome_strict("hello"))
32
+
33
+ def test_palindrome_strict_exact(self):
34
+ self.assertTrue(is_palindrome_strict("noon"))
35
+ self.assertTrue(is_palindrome_strict("level"))
36
+ self.assertFalse(is_palindrome_strict("Hello"))
37
+
38
+ def test_palindrome_with_special_chars(self):
39
+ self.assertTrue(is_palindrome("..."))
40
+ self.assertTrue(is_palindrome("a-b-a"))
41
+
42
+ def test_palindrome_invalid_input(self):
43
+ with self.assertRaises(TypeError):
44
+ is_palindrome(123)
45
+ with self.assertRaises(TypeError):
46
+ is_palindrome(None)
47
+
48
+
49
+ if __name__ == "__main__":
50
+ unittest.main()
File without changes