alpiecode 10.0.1__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.
- {alpiecode-10.0.1 → alpiecode-10.0.3}/PKG-INFO +1 -1
- {alpiecode-10.0.1 → alpiecode-10.0.3}/README.md +86 -12
- {alpiecode-10.0.1 → alpiecode-10.0.3}/pyproject.toml +1 -1
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/alpiecode.egg-info/SOURCES.txt +11 -1
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/__init__.py +1 -1
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/agent.py +37 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/backends/local_backend.py +25 -1
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/cache.py +148 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/cli.py +6 -1
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/context.py +33 -1
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/local_model.py +41 -9
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/memory.py +23 -4
- alpiecode-10.0.3/src/codeagent/metrics.py +101 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/orchestrator.py +47 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/prompt.py +5 -1
- alpiecode-10.0.3/src/codeagent/system1.py +279 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/tools.py +31 -3
- alpiecode-10.0.3/test/test_benchmark_system1.py +90 -0
- alpiecode-10.0.3/test/test_cache_reads.py +66 -0
- alpiecode-10.0.3/test/test_context_filter.py +57 -0
- alpiecode-10.0.3/test/test_math_tools.py +214 -0
- alpiecode-10.0.3/test/test_metrics.py +46 -0
- alpiecode-10.0.3/test/test_offline_opt.py +59 -0
- alpiecode-10.0.3/test/test_palindrome_check.py +50 -0
- alpiecode-10.0.3/test/test_system1.py +84 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/setup.cfg +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/alpiecode/__init__.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/backends/base.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/client.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/compaction.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/config.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/discovery.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/doctor.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/executor.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/git_ops.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/github.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/guardian.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/guardrails.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/media.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/progress.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/rephraser.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/server.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/session.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/updater.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/src/codeagent/vscode_installer.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/test/test_audit_fixes.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/test/test_chat_mode.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/test/test_claude_codex_capabilities.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/test/test_offline_mode.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/test/test_phase1.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/test/test_phase2.py +0 -0
- {alpiecode-10.0.1 → alpiecode-10.0.3}/test/test_phase3.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# AlpieCode
|
|
2
2
|
|
|
3
3
|
[](https://www.python.org/)
|
|
4
|
-
[](https://github.com/169Pi/codeagent-poc)
|
|
5
5
|
[](https://marketplace.visualstudio.com/)
|
|
6
6
|
[](https://fastapi.tiangolo.com/)
|
|
7
7
|
[](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. [
|
|
20
|
-
4. [
|
|
21
|
-
5. [
|
|
22
|
-
6. [
|
|
23
|
-
7. [
|
|
24
|
-
8. [
|
|
25
|
-
9. [
|
|
26
|
-
10. [
|
|
27
|
-
11. [
|
|
28
|
-
12. [
|
|
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
|
|
254
|
+
# Output: alpiecode 10.0.2
|
|
181
255
|
|
|
182
256
|
alpiecode doctor
|
|
183
257
|
```
|
|
@@ -26,12 +26,14 @@ src/codeagent/ipython_ext.py
|
|
|
26
26
|
src/codeagent/local_model.py
|
|
27
27
|
src/codeagent/media.py
|
|
28
28
|
src/codeagent/memory.py
|
|
29
|
+
src/codeagent/metrics.py
|
|
29
30
|
src/codeagent/orchestrator.py
|
|
30
31
|
src/codeagent/progress.py
|
|
31
32
|
src/codeagent/prompt.py
|
|
32
33
|
src/codeagent/rephraser.py
|
|
33
34
|
src/codeagent/server.py
|
|
34
35
|
src/codeagent/session.py
|
|
36
|
+
src/codeagent/system1.py
|
|
35
37
|
src/codeagent/tools.py
|
|
36
38
|
src/codeagent/updater.py
|
|
37
39
|
src/codeagent/vscode_installer.py
|
|
@@ -41,9 +43,17 @@ src/codeagent/backends/local_backend.py
|
|
|
41
43
|
src/codeagent/backends/openai_backend.py
|
|
42
44
|
src/codeagent/extension/alpiecode.vsix
|
|
43
45
|
test/test_audit_fixes.py
|
|
46
|
+
test/test_benchmark_system1.py
|
|
47
|
+
test/test_cache_reads.py
|
|
44
48
|
test/test_chat_mode.py
|
|
45
49
|
test/test_claude_codex_capabilities.py
|
|
50
|
+
test/test_context_filter.py
|
|
51
|
+
test/test_math_tools.py
|
|
52
|
+
test/test_metrics.py
|
|
46
53
|
test/test_offline_mode.py
|
|
54
|
+
test/test_offline_opt.py
|
|
55
|
+
test/test_palindrome_check.py
|
|
47
56
|
test/test_phase1.py
|
|
48
57
|
test/test_phase2.py
|
|
49
|
-
test/test_phase3.py
|
|
58
|
+
test/test_phase3.py
|
|
59
|
+
test/test_system1.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.
|
|
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."""
|
|
@@ -432,6 +432,43 @@ def run_agent(
|
|
|
432
432
|
style="dim"
|
|
433
433
|
)
|
|
434
434
|
|
|
435
|
+
elif event.type == "system1_intervention" and verbose:
|
|
436
|
+
data = event.data
|
|
437
|
+
action = data.get("action", "")
|
|
438
|
+
err_type = data.get("error_type", "")
|
|
439
|
+
directive = data.get("directive", "")
|
|
440
|
+
if HAS_RICH:
|
|
441
|
+
console.print(Panel(
|
|
442
|
+
f"[bold yellow]⚡ System 1 (Jev) Intervention[/bold yellow] (Turn {data.get('turn')})\n"
|
|
443
|
+
f"• Action: [cyan]{action}[/cyan] | Error Type: [red]{err_type}[/red]\n"
|
|
444
|
+
f"• Directive: {directive}",
|
|
445
|
+
border_style="yellow",
|
|
446
|
+
padding=(0, 1)
|
|
447
|
+
))
|
|
448
|
+
else:
|
|
449
|
+
print(f"\n⚡ [System 1 Intervention] {action}: {directive}")
|
|
450
|
+
|
|
451
|
+
elif event.type == "metrics" and verbose:
|
|
452
|
+
data = event.data
|
|
453
|
+
if HAS_RICH:
|
|
454
|
+
console.print(Panel(
|
|
455
|
+
f"⏱️ [bold]Latency:[/bold] {data.get('total_task_latency_s', 0)}s | "
|
|
456
|
+
f"🧠 [bold]LLM Calls:[/bold] {data.get('llm_call_count', 0)} ({data.get('llm_inference_time_ms', 0)}ms)\n"
|
|
457
|
+
f"🛠️ [bold]Tools Time:[/bold] {data.get('total_tool_execution_ms', 0)}ms ({data.get('tool_call_count', 0)} calls) | "
|
|
458
|
+
f"📦 [bold]Context Prep:[/bold] {data.get('context_prep_time_ms', 0)}ms | "
|
|
459
|
+
f"⚡ [bold]Cache Hits:[/bold] {data.get('cache_hits', 0)}",
|
|
460
|
+
title="📊 Task Performance Summary",
|
|
461
|
+
border_style="cyan",
|
|
462
|
+
padding=(0, 1)
|
|
463
|
+
))
|
|
464
|
+
else:
|
|
465
|
+
print(
|
|
466
|
+
f"\n📊 [Metrics] Task Latency: {data.get('total_task_latency_s')}s | "
|
|
467
|
+
f"LLM Calls: {data.get('llm_call_count')} | "
|
|
468
|
+
f"Tools Time: {data.get('total_tool_execution_ms')}ms | "
|
|
469
|
+
f"Cache Hits: {data.get('cache_hits')}"
|
|
470
|
+
)
|
|
471
|
+
|
|
435
472
|
elif event.type == "safety_ceiling" and verbose:
|
|
436
473
|
if HAS_RICH:
|
|
437
474
|
console.print(
|
|
@@ -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:
|
|
@@ -193,3 +193,151 @@ def get_cache() -> ResponseCache:
|
|
|
193
193
|
if _CACHE is None:
|
|
194
194
|
_CACHE = ResponseCache()
|
|
195
195
|
return _CACHE
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ── File Read & Test Result Caching ─────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
class FileReadCache:
|
|
201
|
+
"""In-memory cache for repeated file reads keyed by (path, mtime, size, start_line, end_line)."""
|
|
202
|
+
|
|
203
|
+
def __init__(self, max_entries: int = 1000):
|
|
204
|
+
self.max_entries = max_entries
|
|
205
|
+
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
206
|
+
self.hits = 0
|
|
207
|
+
self.misses = 0
|
|
208
|
+
|
|
209
|
+
def get(self, file_path: Path, start_line: Optional[int] = None, end_line: Optional[int] = None) -> Optional[str]:
|
|
210
|
+
try:
|
|
211
|
+
st = file_path.stat()
|
|
212
|
+
key = f"{file_path.resolve()}:{st.st_mtime}:{st.st_size}:{start_line}:{end_line}"
|
|
213
|
+
except OSError:
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
entry = self._cache.get(key)
|
|
217
|
+
if entry is not None:
|
|
218
|
+
self.hits += 1
|
|
219
|
+
return entry["content"]
|
|
220
|
+
|
|
221
|
+
self.misses += 1
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
def put(self, file_path: Path, content: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> None:
|
|
225
|
+
try:
|
|
226
|
+
st = file_path.stat()
|
|
227
|
+
key = f"{file_path.resolve()}:{st.st_mtime}:{st.st_size}:{start_line}:{end_line}"
|
|
228
|
+
except OSError:
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
if len(self._cache) >= self.max_entries:
|
|
232
|
+
keys = list(self._cache.keys())[: max(1, self.max_entries // 5)]
|
|
233
|
+
for k in keys:
|
|
234
|
+
self._cache.pop(k, None)
|
|
235
|
+
|
|
236
|
+
self._cache[key] = {"content": content, "cached_at": time.time()}
|
|
237
|
+
|
|
238
|
+
def invalidate(self, file_path: Optional[Path] = None) -> None:
|
|
239
|
+
if file_path is None:
|
|
240
|
+
self._cache.clear()
|
|
241
|
+
return
|
|
242
|
+
prefix = f"{file_path.resolve()}:"
|
|
243
|
+
keys_to_del = [k for k in self._cache if k.startswith(prefix)]
|
|
244
|
+
for k in keys_to_del:
|
|
245
|
+
self._cache.pop(k, None)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class TestResultCache:
|
|
249
|
+
"""Caches test command execution results when workspace files are unchanged.
|
|
250
|
+
Persists across CLI runs in ~/.alpiecode/cache/test_cache.json
|
|
251
|
+
"""
|
|
252
|
+
__test__ = False
|
|
253
|
+
|
|
254
|
+
def __init__(self, max_entries: int = 100):
|
|
255
|
+
self.max_entries = max_entries
|
|
256
|
+
self.disk_file = CACHE_DIR / "test_cache.json"
|
|
257
|
+
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
258
|
+
self.hits = 0
|
|
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
|
|
275
|
+
|
|
276
|
+
def _is_test_cmd(self, cmd: str) -> bool:
|
|
277
|
+
c = cmd.strip().lower()
|
|
278
|
+
return (
|
|
279
|
+
"pytest" in c
|
|
280
|
+
or "unittest" in c
|
|
281
|
+
or "npm test" in c
|
|
282
|
+
or "cargo test" in c
|
|
283
|
+
or "go test" in c
|
|
284
|
+
)
|
|
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
|
+
|
|
297
|
+
def get(self, workdir: Path, cmd: str) -> Optional[str]:
|
|
298
|
+
if not self._is_test_cmd(cmd):
|
|
299
|
+
return None
|
|
300
|
+
version = self._get_workspace_fingerprint(workdir)
|
|
301
|
+
key = f"{workdir.resolve()}:{cmd.strip()}:{version}"
|
|
302
|
+
entry = self._cache.get(key)
|
|
303
|
+
if entry is not None:
|
|
304
|
+
self.hits += 1
|
|
305
|
+
return f"[Cached test result]\n{entry['content']}"
|
|
306
|
+
self.misses += 1
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
def put(self, workdir: Path, cmd: str, result: str) -> None:
|
|
310
|
+
if not self._is_test_cmd(cmd):
|
|
311
|
+
return
|
|
312
|
+
version = self._get_workspace_fingerprint(workdir)
|
|
313
|
+
key = f"{workdir.resolve()}:{cmd.strip()}:{version}"
|
|
314
|
+
self._cache[key] = {"content": result, "cached_at": time.time()}
|
|
315
|
+
self._save_disk()
|
|
316
|
+
|
|
317
|
+
def invalidate(self, workdir: Optional[Path] = None) -> None:
|
|
318
|
+
if workdir is None:
|
|
319
|
+
self._cache.clear()
|
|
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()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
_FILE_CACHE: Optional[FileReadCache] = None
|
|
329
|
+
_TEST_CACHE: Optional[TestResultCache] = None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def get_file_cache() -> FileReadCache:
|
|
333
|
+
global _FILE_CACHE
|
|
334
|
+
if _FILE_CACHE is None:
|
|
335
|
+
_FILE_CACHE = FileReadCache()
|
|
336
|
+
return _FILE_CACHE
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def get_test_cache() -> TestResultCache:
|
|
340
|
+
global _TEST_CACHE
|
|
341
|
+
if _TEST_CACHE is None:
|
|
342
|
+
_TEST_CACHE = TestResultCache()
|
|
343
|
+
return _TEST_CACHE
|
|
@@ -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
|
-
|
|
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):
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
"""
|
|
2
3
|
Conversation context manager for AlpieCode.
|
|
3
4
|
|
|
@@ -198,10 +199,11 @@ class ContextManager:
|
|
|
198
199
|
) -> None:
|
|
199
200
|
"""Append a tool execution result and record metadata."""
|
|
200
201
|
self.invalidate_cache()
|
|
202
|
+
filtered_content = filter_noisy_tool_output(content, tool_name=tool_name)
|
|
201
203
|
msg = {
|
|
202
204
|
"role": "tool",
|
|
203
205
|
"tool_call_id": tool_call_id,
|
|
204
|
-
"content":
|
|
206
|
+
"content": filtered_content,
|
|
205
207
|
}
|
|
206
208
|
self._messages.append(dict(msg))
|
|
207
209
|
self._raw_messages.append(copy.deepcopy(msg))
|
|
@@ -415,3 +417,33 @@ class ContextManager:
|
|
|
415
417
|
self._cached_summary = extract_conversation_summary(self._messages, self._metadata)
|
|
416
418
|
return True
|
|
417
419
|
return False
|
|
420
|
+
|
|
421
|
+
def filter_noisy_tool_output(content: str, tool_name: Optional[str] = None) -> str:
|
|
422
|
+
"""
|
|
423
|
+
Filter noisy tool output to conserve LLM context window.
|
|
424
|
+
Extracts relevant errors/failures and condenses passing test spam.
|
|
425
|
+
"""
|
|
426
|
+
if not content or len(content) < 300:
|
|
427
|
+
return content
|
|
428
|
+
|
|
429
|
+
# 1. Pytest / Unittest clean pass compression
|
|
430
|
+
if "passed in" in content and ("test session starts" in content or "collected" in content):
|
|
431
|
+
if "FAILED" not in content and "ERROR" not in content and "failed" not in content:
|
|
432
|
+
m = re.search(r"(=+\s*\d+\s+passed.*in\s+[\d\.]+s\s*=+)", content)
|
|
433
|
+
if m:
|
|
434
|
+
return f"[Clean Test Output]\n{m.group(1)}"
|
|
435
|
+
else:
|
|
436
|
+
# Tests had failures: strip passing test lines, keep failure traceback
|
|
437
|
+
lines = content.splitlines()
|
|
438
|
+
filtered = [
|
|
439
|
+
line for line in lines
|
|
440
|
+
if not (" PASSED" in line and ("[" in line and "%]" in line))
|
|
441
|
+
]
|
|
442
|
+
return "\n".join(filtered)
|
|
443
|
+
|
|
444
|
+
if "Ran " in content and " tests in " in content and "OK" in content and "FAILED" not in content:
|
|
445
|
+
m = re.search(r"(Ran \d+ tests? in [\d\.]+s\s+OK)", content)
|
|
446
|
+
if m:
|
|
447
|
+
return f"[Clean Test Output]\n{m.group(1)}"
|
|
448
|
+
|
|
449
|
+
return content
|
|
@@ -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"
|
|
253
|
-
[sys.executable, "-m", "pip", "install"
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
477
|
-
cleaned = re.sub(r"^\s*(?:</think>|<think>)*\s*", "",
|
|
478
|
-
msg_dict["content"] =
|
|
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(
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
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. "
|