alpiecode 10.0.3__tar.gz → 10.5.1__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 (64) hide show
  1. {alpiecode-10.0.3 → alpiecode-10.5.1}/PKG-INFO +1 -1
  2. {alpiecode-10.0.3 → alpiecode-10.5.1}/pyproject.toml +1 -1
  3. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/alpiecode.egg-info/SOURCES.txt +3 -15
  5. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/__init__.py +1 -1
  6. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/agent.py +2 -1
  7. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/backends/local_backend.py +34 -11
  8. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/backends/openai_backend.py +14 -3
  9. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/cli.py +78 -14
  10. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/compaction.py +122 -10
  11. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/config.py +2 -2
  12. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/context.py +43 -14
  13. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/discovery.py +5 -0
  14. alpiecode-10.5.1/src/codeagent/dream_rsi.py +374 -0
  15. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/executor.py +17 -6
  16. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/local_model.py +72 -11
  17. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/orchestrator.py +154 -17
  18. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/prompt.py +11 -3
  19. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/system1.py +99 -18
  20. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/tools.py +27 -0
  21. alpiecode-10.5.1/test/test_context_and_trajectory.py +337 -0
  22. alpiecode-10.5.1/test/test_dream_rsi.py +478 -0
  23. alpiecode-10.0.3/test/test_audit_fixes.py +0 -71
  24. alpiecode-10.0.3/test/test_benchmark_system1.py +0 -90
  25. alpiecode-10.0.3/test/test_cache_reads.py +0 -66
  26. alpiecode-10.0.3/test/test_chat_mode.py +0 -85
  27. alpiecode-10.0.3/test/test_claude_codex_capabilities.py +0 -111
  28. alpiecode-10.0.3/test/test_context_filter.py +0 -57
  29. alpiecode-10.0.3/test/test_math_tools.py +0 -214
  30. alpiecode-10.0.3/test/test_metrics.py +0 -46
  31. alpiecode-10.0.3/test/test_offline_mode.py +0 -314
  32. alpiecode-10.0.3/test/test_offline_opt.py +0 -59
  33. alpiecode-10.0.3/test/test_palindrome_check.py +0 -50
  34. alpiecode-10.0.3/test/test_phase1.py +0 -302
  35. alpiecode-10.0.3/test/test_phase2.py +0 -231
  36. alpiecode-10.0.3/test/test_phase3.py +0 -154
  37. alpiecode-10.0.3/test/test_system1.py +0 -84
  38. {alpiecode-10.0.3 → alpiecode-10.5.1}/README.md +0 -0
  39. {alpiecode-10.0.3 → alpiecode-10.5.1}/setup.cfg +0 -0
  40. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/alpiecode/__init__.py +0 -0
  41. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  42. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
  43. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/alpiecode.egg-info/requires.txt +0 -0
  44. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/alpiecode.egg-info/top_level.txt +0 -0
  45. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/backends/__init__.py +0 -0
  46. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/backends/base.py +0 -0
  47. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/cache.py +0 -0
  48. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/client.py +0 -0
  49. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/doctor.py +0 -0
  50. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/extension/alpiecode.vsix +0 -0
  51. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/git_ops.py +0 -0
  52. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/github.py +0 -0
  53. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/guardian.py +0 -0
  54. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/guardrails.py +0 -0
  55. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/ipython_ext.py +0 -0
  56. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/media.py +0 -0
  57. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/memory.py +0 -0
  58. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/metrics.py +0 -0
  59. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/progress.py +0 -0
  60. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/rephraser.py +0 -0
  61. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/server.py +0 -0
  62. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/session.py +0 -0
  63. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/updater.py +0 -0
  64. {alpiecode-10.0.3 → alpiecode-10.5.1}/src/codeagent/vscode_installer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 10.0.3
3
+ Version: 10.5.1
4
4
  Summary: AlpieCode
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 = "10.0.3"
7
+ version = "10.5.1"
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.3
3
+ Version: 10.5.1
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -17,6 +17,7 @@ src/codeagent/config.py
17
17
  src/codeagent/context.py
18
18
  src/codeagent/discovery.py
19
19
  src/codeagent/doctor.py
20
+ src/codeagent/dream_rsi.py
20
21
  src/codeagent/executor.py
21
22
  src/codeagent/git_ops.py
22
23
  src/codeagent/github.py
@@ -42,18 +43,5 @@ src/codeagent/backends/base.py
42
43
  src/codeagent/backends/local_backend.py
43
44
  src/codeagent/backends/openai_backend.py
44
45
  src/codeagent/extension/alpiecode.vsix
45
- test/test_audit_fixes.py
46
- test/test_benchmark_system1.py
47
- test/test_cache_reads.py
48
- test/test_chat_mode.py
49
- test/test_claude_codex_capabilities.py
50
- test/test_context_filter.py
51
- test/test_math_tools.py
52
- test/test_metrics.py
53
- test/test_offline_mode.py
54
- test/test_offline_opt.py
55
- test/test_palindrome_check.py
56
- test/test_phase1.py
57
- test/test_phase2.py
58
- test/test_phase3.py
59
- test/test_system1.py
46
+ test/test_context_and_trajectory.py
47
+ test/test_dream_rsi.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.3"
20
+ __version__ = "10.5.1"
21
21
 
22
22
  def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
23
23
  """Run an autonomous coding task programmatically."""
@@ -340,7 +340,8 @@ def run_agent(
340
340
  console.print(f"📺 URL: {url}", style="cyan")
341
341
 
342
342
  elif event.type == "adaptive_mode" and verbose and HAS_RICH:
343
- console.print("⚡ [dim]Adaptive mode: simple task detected, skipping deep reasoning for speed[/dim]")
343
+ msg = event.data.get("message") or "Adaptive mode: reasoning trace adjusted."
344
+ console.print(f"⚡ [dim]{msg}[/dim]")
344
345
 
345
346
  elif event.type == "status":
346
347
  phase = event.data.get("phase")
@@ -91,17 +91,38 @@ class LocalBackend:
91
91
  elif not isinstance(content, str):
92
92
  content = str(content)
93
93
 
94
- # For assistant messages with tool_calls, embed tool call info in content text
94
+ # For assistant messages, condense bulky write_file / edit_file payloads from content
95
+ if role == "assistant":
96
+ from ..compaction import compact_text_tool_calls
97
+ content = compact_text_tool_calls(content)
98
+
99
+ # For assistant messages with tool_calls, embed tool call info in content text ONLY if not already marked
95
100
  if role == "assistant" and msg.get("tool_calls"):
96
- tc_descriptions = []
97
- for tc in msg["tool_calls"]:
98
- if isinstance(tc, dict):
99
- fn = tc.get("function", {})
100
- name = fn.get("name", "unknown")
101
- args = fn.get("arguments", "{}")
102
- tc_descriptions.append(f"[Called tool: {name}({args})]")
103
- if tc_descriptions:
104
- content = (content + "\n" + "\n".join(tc_descriptions)).strip()
101
+ already_marked = (
102
+ "[Completed " in content
103
+ or "[Incomplete" in content
104
+ or "[Called tool:" in content
105
+ or "<tool_call>" in content
106
+ )
107
+ if not already_marked:
108
+ tc_descriptions = []
109
+ for tc in msg["tool_calls"]:
110
+ if isinstance(tc, dict):
111
+ fn = tc.get("function", {})
112
+ name = fn.get("name", "unknown")
113
+ args = fn.get("arguments", "{}")
114
+ # Do not duplicate full file content in context history
115
+ if name in ("write_file", "edit_file", "apply_patch"):
116
+ try:
117
+ import json as _json
118
+ a_dict = _json.loads(args) if isinstance(args, str) else args
119
+ p = a_dict.get("path") or a_dict.get("filename") or a_dict.get("file") or ""
120
+ args = f'{{"path": "{p}"}}'
121
+ except Exception:
122
+ args = "{...}"
123
+ tc_descriptions.append(f"[Called tool: {name}({args})]")
124
+ if tc_descriptions:
125
+ content = (content + "\n" + "\n".join(tc_descriptions)).strip()
105
126
  # Do NOT include tool_calls key — GGUF template will crash on it
106
127
 
107
128
  clean["content"] = content
@@ -247,7 +268,9 @@ class LocalBackend:
247
268
  inside_suppressed_think = True
248
269
  text = text.replace("<think>", "")
249
270
  if inside_suppressed_think:
250
- if "</think>" in text:
271
+ if "<tool_call>" in text or "DONE:" in text or "<function=" in text:
272
+ inside_suppressed_think = False
273
+ elif "</think>" in text:
251
274
  _, after = text.split("</think>", 1)
252
275
  inside_suppressed_think = False
253
276
  text = after
@@ -54,14 +54,20 @@ def _repair_and_parse_json_arguments(raw_args: str) -> dict:
54
54
  # If trailing comma at end: `{"a": 1,` -> `{"a": 1`
55
55
  repaired = re.sub(r",\s*$", "", repaired)
56
56
 
57
+ was_truncated = False
57
58
  # Check unclosed quotes: count non-escaped double quotes
58
59
  quote_count = len(re.findall(r'(?<!\\)"', repaired))
59
60
  if quote_count % 2 != 0:
61
+ was_truncated = True
62
+ dangling_bs = len(repaired) - len(repaired.rstrip('\\'))
63
+ if dangling_bs % 2 != 0:
64
+ repaired = repaired[:-1]
60
65
  repaired += '"'
61
66
 
62
- # Balance unclosed brackets and braces
63
- open_curly = repaired.count("{") - repaired.count("}")
64
- open_square = repaired.count("[") - repaired.count("]")
67
+ # Balance unclosed brackets and braces outside string literals
68
+ stripped = re.sub(r'"(?:\\.|[^"\\])*"', '""', repaired)
69
+ open_curly = stripped.count("{") - stripped.count("}")
70
+ open_square = stripped.count("[") - stripped.count("]")
65
71
 
66
72
  if open_square > 0:
67
73
  repaired += "]" * open_square
@@ -71,6 +77,8 @@ def _repair_and_parse_json_arguments(raw_args: str) -> dict:
71
77
  try:
72
78
  parsed = json.loads(repaired)
73
79
  if isinstance(parsed, dict):
80
+ if was_truncated:
81
+ parsed["_was_truncated"] = True
74
82
  return parsed
75
83
  except Exception:
76
84
  pass
@@ -91,6 +99,9 @@ def _repair_and_parse_json_arguments(raw_args: str) -> dict:
91
99
  extracted[k] = v_lit
92
100
 
93
101
  if extracted:
102
+ if "name" in extracted and "arguments" not in extracted:
103
+ args = {k: v for k, v in extracted.items() if k not in ("name", "id", "type")}
104
+ return {"name": extracted["name"], "arguments": args}
94
105
  return extracted
95
106
 
96
107
  logger.warning("Failed to parse tool call arguments: %s", raw_args[:200])
@@ -29,18 +29,30 @@ def _show_banner():
29
29
 
30
30
 
31
31
  def _normalize_args():
32
- known_commands = {"init", "serve", "run", "chat", "plan", "diff", "undo", "doctor", "explain", "version", "-h", "--help", "-v", "--version"}
32
+ known_commands = {"init", "serve", "run", "chat", "plan", "diff", "undo", "doctor", "explain", "dream", "version", "-h", "--help", "-v", "--version"}
33
33
  subcommand = None
34
-
34
+
35
+ # Normalize common flag typos / aliases before parsing
36
+ for idx, arg in enumerate(sys.argv):
37
+ if arg in ("--offlie", "-offline"):
38
+ sys.argv[idx] = "--offline"
39
+
35
40
  # Check if a subcommand is present
36
41
  for arg in sys.argv[1:]:
37
42
  if arg in known_commands:
38
43
  subcommand = arg
39
44
  break
40
-
45
+
41
46
  if not subcommand and len(sys.argv) > 1:
42
- subcommand = "run"
43
- sys.argv.insert(1, "run")
47
+ # Determine if any positional non-flag argument was provided
48
+ positional_args = [a for a in sys.argv[1:] if not a.startswith("-")]
49
+ if not positional_args:
50
+ # Flags like --offline or --thinking given without a subcommand or task -> launch chat mode
51
+ subcommand = "chat"
52
+ sys.argv.insert(1, "chat")
53
+ else:
54
+ subcommand = "run"
55
+ sys.argv.insert(1, "run")
44
56
  elif subcommand and subcommand not in ("-h", "--help", "-v", "--version") and sys.argv[1] != subcommand:
45
57
  # Move subcommand to index 1 so subparsers parse arguments correctly without default override
46
58
  sys.argv.remove(subcommand)
@@ -49,11 +61,11 @@ def _normalize_args():
49
61
  if subcommand in ("run", "plan", "explain") and len(sys.argv) > 2:
50
62
  cmd_idx = sys.argv.index(subcommand)
51
63
  sub_args = sys.argv[cmd_idx + 1:]
52
-
64
+
53
65
  flags = []
54
66
  positionals = []
55
- value_flags = {"--workdir", "--image", "--video", "--url", "--github", "--max-turns", "--host", "--port"}
56
-
67
+ value_flags = {"--workdir", "--image", "--video", "--url", "--github", "--max-turns", "--host", "--port", "--ctx-size"}
68
+
57
69
  i = 0
58
70
  while i < len(sub_args):
59
71
  arg = sub_args[i]
@@ -67,7 +79,7 @@ def _normalize_args():
67
79
  else:
68
80
  positionals.append(arg)
69
81
  i += 1
70
-
82
+
71
83
  sys.argv = sys.argv[:cmd_idx + 1] + flags + positionals
72
84
 
73
85
 
@@ -87,7 +99,7 @@ def main():
87
99
  common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
88
100
  common.add_argument("--debug", action="store_true", help="Show autonomous discovery and debug diagnostics")
89
101
  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)")
102
+ common.add_argument("--ctx-size", type=int, default=None, help="Context size in tokens (default: 8192 offline, 262144 online)")
91
103
 
92
104
  from . import __version__
93
105
  parser = argparse.ArgumentParser(
@@ -103,7 +115,7 @@ def main():
103
115
  sub.add_parser("init", help="Configure your VLM/OpenAI-compatible endpoint")
104
116
 
105
117
  # ── serve ──
106
- serve_p = sub.add_parser("serve", help="Start the AlpieCode API server")
118
+ serve_p = sub.add_parser("serve", help="Start the AlpieCode API server", parents=[common])
107
119
  serve_p.add_argument("--host", default="127.0.0.1", help="Host address to bind to (default: 127.0.0.1)")
108
120
  serve_p.add_argument("--port", type=int, default=7169, help="Port to listen on (default: 7169)")
109
121
 
@@ -125,12 +137,18 @@ def main():
125
137
  undo_p = sub.add_parser("undo", help="Revert file changes made by the last AlpieCode session", parents=[common])
126
138
 
127
139
  # ── doctor ──
128
- sub.add_parser("doctor", help="Run system health checks (CUDA, Python, network, dev tools)")
140
+ sub.add_parser("doctor", help="Run system health checks (CUDA, Python, network, dev tools)", parents=[common])
129
141
 
130
142
  # ── explain ──
131
143
  explain_p = sub.add_parser("explain", help="Explain a file, function, or codebase concept", parents=[common])
132
144
  explain_p.add_argument("target", help="File path or concept to explain")
133
145
 
146
+ # ?? dream ??
147
+ dream_p = sub.add_parser('dream', help='Dream-RSI: offline recursive self-improvement simulation', parents=[common])
148
+ dream_p.add_argument('--optimize', action='store_true', help='Run offline dreaming simulation to synthesize optimal search policy')
149
+ dream_p.add_argument('--stats', action='store_true', help='Show trajectory replay metrics and active policy status')
150
+ dream_p.add_argument('--clear', action='store_true', help='Clear historical trajectory replay pool')
151
+
134
152
  args = parser.parse_args()
135
153
 
136
154
  if not args.command:
@@ -145,6 +163,52 @@ def main():
145
163
  interactive_init()
146
164
  return
147
165
 
166
+ if args.command == 'dream':
167
+ import time
168
+ from .dream_rsi import TrajectoryStore, DreamOptimizer, load_active_dream_profile
169
+ store = TrajectoryStore()
170
+ optimizer = DreamOptimizer(store)
171
+ if getattr(args, 'clear', False):
172
+ count = store.clear_trajectories()
173
+ print(f'?? Cleared {count} historical trajectories from replay pool.')
174
+ return
175
+ if getattr(args, 'optimize', False):
176
+ print('?? Running Dream-RSI offline recursive self-improvement simulation...')
177
+ profile, res = optimizer.optimize()
178
+ if res.get('status') == 'no_trajectories':
179
+ print('?? No historical trajectories recorded yet. Run tasks with alpiecode run to populate the replay buffer.')
180
+ return
181
+ metrics = res.get('metrics', {})
182
+ print('\n? Dream-RSI Optimization Complete!')
183
+ print(f' ?? Trajectories Replayed: {metrics.get("total_trajectories", 0)}')
184
+ print(f' ?? Total Turns Saved: {metrics.get("total_turns_saved", 0)}')
185
+ print(f' ?? Loops Prevented: {metrics.get("total_loops_prevented", 0)}')
186
+ print(f' ?? Stalls Averted: {metrics.get("total_stalls_averted", 0)}')
187
+ print(f' ? Simulation Fitness: {metrics.get("average_score", 0.0)}')
188
+ print('\n?? Active Policy Updated:')
189
+ print(f' ? Syntax Loop Threshold: {profile.syntax_loop_threshold}')
190
+ print(f' ? General Loop Threshold: {profile.general_loop_threshold}')
191
+ print(f' ? Stall Trigger: {profile.stall_threshold}')
192
+ print(f' ? Single-File Enforcement: {profile.single_file_enforcement}')
193
+ return
194
+ trajectories = store.list_trajectories()
195
+ profile = load_active_dream_profile()
196
+ successes = sum(1 for t in trajectories if t.outcome == 'success')
197
+ failures = sum(1 for t in trajectories if t.outcome != 'success')
198
+ print('\n?? Dream-RSI Status & Replay Pool')
199
+ print(f' ?? Replay Buffer: {len(trajectories)} trajectories stored')
200
+ print(f' ? Successful Runs: {successes}')
201
+ print(f' ?? Failed/Stalled Runs: {failures}')
202
+ print('\n?? Active Policy Profile:')
203
+ print(f' ? Syntax Loop Threshold: {profile.syntax_loop_threshold}')
204
+ print(f' ? General Loop Threshold: {profile.general_loop_threshold}')
205
+ print(f' ? Stall Trigger: {profile.stall_threshold}')
206
+ print(f' ? Single-File Enforcement: {profile.single_file_enforcement}')
207
+ opt_time = time.ctime(profile.last_optimized_at) if profile.last_optimized_at else 'Never'
208
+ print(f' ? Last Optimized: {opt_time}')
209
+ print('\n?? Tip: Run alpiecode dream --optimize to run offline self-improvement.')
210
+ return
211
+
148
212
  if args.command == "doctor":
149
213
  from .doctor import run_doctor
150
214
  sys.exit(run_doctor())
@@ -175,9 +239,9 @@ def main():
175
239
 
176
240
  if getattr(args, "offline", False):
177
241
  cfg.base_url = "offline"
178
- # 4096 default fits 4.4GB model + KV cache 100% in 6GB VRAM with zero host-paging
242
+ # 8192 default fits 4.4GB model + KV cache 100% in 6GB VRAM (4.75GB / 6GB) with zero host-paging
179
243
  import os
180
- cfg.n_ctx = int(os.environ.get("ALPIECODE_CTX_SIZE", 4096))
244
+ cfg.n_ctx = int(os.environ.get("ALPIECODE_CTX_SIZE", 8192))
181
245
  os.environ["ALPIECODE_BASE_URL"] = "offline"
182
246
 
183
247
  if getattr(args, "ctx_size", None):
@@ -15,6 +15,7 @@ Strategy:
15
15
  """
16
16
 
17
17
  import json
18
+ import re
18
19
  from typing import Any, Dict, List, Optional
19
20
 
20
21
  # Our model's context window
@@ -219,27 +220,120 @@ def select_relevant_history(
219
220
  return "### Relevant Historical Anchors:\n" + "\n".join(reversed(high_value_notes))
220
221
 
221
222
 
222
- def compact_messages(messages: List[dict]) -> List[dict]:
223
+ def compact_text_tool_calls(text: str) -> str:
224
+ """Condense bulky file content in text-based tool calls."""
225
+ if not text or ("write_file" not in text and "edit_file" not in text and "apply_patch" not in text):
226
+ return text
227
+
228
+ def _replace_block(m):
229
+ raw = m.group(1).strip()
230
+ try:
231
+ from .backends.openai_backend import _repair_and_parse_json_arguments
232
+ data = _repair_and_parse_json_arguments(raw)
233
+ if isinstance(data, dict) and data.get("name") in ("write_file", "edit_file", "apply_patch"):
234
+ args = data.get("arguments", {})
235
+ if isinstance(args, dict):
236
+ p = args.get("path") or args.get("filename") or args.get("file") or "file"
237
+ return f"[Completed {data['name']} for '{p}']"
238
+ except Exception:
239
+ pass
240
+ return m.group(0)
241
+
242
+ # 1. First condense complete/closed <tool_call>...</tool_call> blocks
243
+ text = re.sub(r"<tool_call>\s*(.*?)\s*</tool_call>", _replace_block, text, flags=re.DOTALL)
244
+
245
+ # 2. Also condense unclosed/truncated <tool_call> blocks (e.g. from token cutoff before </tool_call>)
246
+ if "<tool_call>" in text:
247
+ def _replace_unclosed(m):
248
+ raw = m.group(0)
249
+ t_name = "write_file"
250
+ p = "file"
251
+ name_m = re.search(r'"name":\s*"([^"]+)"', raw)
252
+ if name_m:
253
+ t_name = name_m.group(1)
254
+ path_m = re.search(r'"(?:path|filename|file)":\s*"([^"]+)"', raw)
255
+ if path_m:
256
+ p = path_m.group(1)
257
+ return f"[Incomplete/truncated {t_name} for '{p}']"
258
+
259
+ text = re.sub(r"<tool_call>(?:(?!</tool_call>).)*$", _replace_unclosed, text, flags=re.DOTALL)
260
+
261
+ return text
262
+
263
+
264
+ def compact_tool_calls(tool_calls: List[Any]) -> List[Any]:
265
+ """Condense bulky file content in historical tool calls to conserve context tokens."""
266
+ if not tool_calls:
267
+ return []
268
+ compacted = []
269
+ for tc in tool_calls:
270
+ if not isinstance(tc, dict):
271
+ compacted.append(tc)
272
+ continue
273
+ fn = tc.get("function", {})
274
+ if not isinstance(fn, dict):
275
+ compacted.append(tc)
276
+ continue
277
+ name = fn.get("name", "")
278
+ raw_args = fn.get("arguments", "")
279
+ if name in ("write_file", "edit_file", "apply_patch"):
280
+ try:
281
+ args = json.loads(raw_args) if isinstance(raw_args, str) else dict(raw_args)
282
+ content = args.get("content") or args.get("patch") or ""
283
+ path = args.get("path") or args.get("filename") or "file"
284
+ if isinstance(content, str) and len(content) > 150:
285
+ lines = content.splitlines()
286
+ args["content"] = f"[Content saved to {path} ({len(lines)} lines, {len(content)} bytes)]"
287
+ compact_tc = dict(tc)
288
+ compact_tc["function"] = dict(fn)
289
+ compact_tc["function"]["arguments"] = json.dumps(args) if isinstance(raw_args, str) else args
290
+ compacted.append(compact_tc)
291
+ continue
292
+ except Exception:
293
+ pass
294
+ compacted.append(tc)
295
+ return compacted
296
+
297
+
298
+ def compact_messages(messages: List[dict], keep_recent: int = KEEP_RECENT_TURNS) -> List[dict]:
223
299
  """
224
300
  Compact a message list by summarizing older turns.
225
301
 
226
302
  Preserves:
227
303
  - System prompt (index 0)
228
304
  - All user messages (verbatim)
229
- - Last KEEP_RECENT_TURNS messages (verbatim)
305
+ - Last keep_recent messages (verbatim, with older tool arguments trimmed if bulky)
230
306
 
231
307
  Summarizes:
232
308
  - Older tool results (truncated)
233
309
  - Older assistant reasoning (removed)
310
+ - Older write_file/edit_file payload content (condensed)
234
311
  """
235
- if len(messages) <= KEEP_RECENT_TURNS + 2:
236
- return messages
312
+ if len(messages) <= keep_recent + 2:
313
+ # Even if short, condense older bulky assistant tool calls while keeping latest intact
314
+ result = []
315
+ last_asst_idx = -1
316
+ for i in range(len(messages) - 1, -1, -1):
317
+ if messages[i].get("role") == "assistant":
318
+ last_asst_idx = i
319
+ break
320
+ for i, m in enumerate(messages):
321
+ if i != last_asst_idx and m.get("role") == "assistant":
322
+ cm = dict(m)
323
+ if cm.get("tool_calls"):
324
+ cm["tool_calls"] = compact_tool_calls(m["tool_calls"])
325
+ if cm.get("content"):
326
+ cm["content"] = compact_text_tool_calls(cm["content"])
327
+ result.append(cm)
328
+ else:
329
+ result.append(m)
330
+ return result
237
331
 
238
332
  # Always keep system prompt
239
333
  system = messages[0] if messages and messages[0].get("role") == "system" else None
240
334
 
241
335
  # Split into old and recent
242
- cutoff = len(messages) - KEEP_RECENT_TURNS
336
+ cutoff = len(messages) - keep_recent
243
337
 
244
338
  # ── Tool-Call Pairing Guard ──
245
339
  # If cutoff lands on a 'tool' message, move it backwards to include
@@ -263,11 +357,11 @@ def compact_messages(messages: List[dict]) -> List[dict]:
263
357
  compacted_old.append(msg)
264
358
 
265
359
  elif role == "assistant":
266
- # Compact assistant messages: keep tool calls but remove reasoning
360
+ # Compact assistant messages: keep tool calls (condensed) but remove reasoning
267
361
  compact_msg = {"role": "assistant"}
268
362
  if msg.get("content"):
269
363
  # Truncate long assistant content
270
- content = msg["content"]
364
+ content = compact_text_tool_calls(msg["content"])
271
365
  if len(content) > 200:
272
366
  compact_msg["content"] = content[:200] + "..."
273
367
  else:
@@ -276,7 +370,7 @@ def compact_messages(messages: List[dict]) -> List[dict]:
276
370
  compact_msg["content"] = None
277
371
 
278
372
  if msg.get("tool_calls"):
279
- compact_msg["tool_calls"] = msg["tool_calls"]
373
+ compact_msg["tool_calls"] = compact_tool_calls(msg["tool_calls"])
280
374
  compacted_old.append(compact_msg)
281
375
 
282
376
  elif role == "tool":
@@ -301,11 +395,29 @@ def compact_messages(messages: List[dict]) -> List[dict]:
301
395
  "content": _summarize_tool_result(tool_name, content),
302
396
  })
303
397
 
398
+ # In recent messages, also condense older assistant tool calls (except the immediate last one)
399
+ last_asst_idx = -1
400
+ for i in range(len(recent_messages) - 1, -1, -1):
401
+ if recent_messages[i].get("role") == "assistant":
402
+ last_asst_idx = i
403
+ break
404
+
405
+ cleaned_recent = []
406
+ for i, m in enumerate(recent_messages):
407
+ if i != last_asst_idx and m.get("role") == "assistant":
408
+ cm = dict(m)
409
+ if cm.get("tool_calls"):
410
+ cm["tool_calls"] = compact_tool_calls(m["tool_calls"])
411
+ if cm.get("content"):
412
+ cm["content"] = compact_text_tool_calls(cm["content"])
413
+ cleaned_recent.append(cm)
414
+ else:
415
+ cleaned_recent.append(m)
416
+
304
417
  # Rebuild message list
305
418
  result = []
306
419
  if system:
307
420
  result.append(system)
308
421
  result.extend(compacted_old)
309
- result.extend(recent_messages)
310
-
422
+ result.extend(cleaned_recent)
311
423
  return result
@@ -158,8 +158,8 @@ def load_config() -> Config:
158
158
  # For offline mode, set high-performance 16k context (16384 tokens) for big multi-file projects
159
159
  # Fits completely in 6GB VRAM on RTX 3050 with zero paging
160
160
  if data.get("base_url") == "offline" and not any(os.environ.get(v) for v in ("ALPIECODE_N_CTX", "ALPIECODE_CTX_SIZE")):
161
- if data.get("n_ctx") in (32768, 4096):
162
- data["n_ctx"] = 16384
161
+ if data.get("n_ctx") in (32768, 4096, 16384):
162
+ data["n_ctx"] = 8192
163
163
  if data.get("max_tokens", 8192) > 4096:
164
164
  data["max_tokens"] = 4096
165
165
 
@@ -14,6 +14,8 @@ from typing import Any, Dict, List, Optional
14
14
 
15
15
  from .backends.base import ChatResponse, ToolCall
16
16
  from .compaction import (
17
+ compact_tool_calls,
18
+ compact_text_tool_calls,
17
19
  CHARS_PER_TOKEN,
18
20
  compact_messages,
19
21
  estimate_tokens,
@@ -350,25 +352,42 @@ class ContextManager:
350
352
  context_blocks.append(dict(m))
351
353
  break
352
354
 
355
+ # ── Budget Verification & Headroom Limits ──
356
+ effective_limit = max_tokens or self.max_tokens
357
+
353
358
  # ── Layer 4 & 5: Recent Messages and Current Request ──
354
- if len(recent_messages) > 6:
355
- for i in range(len(recent_messages) - 4):
356
- m = recent_messages[i]
357
- if m.get("role") == "tool":
358
- c = str(m.get("content") or "")
359
- if len(c) > 400 and "[Plan updated]" not in c and "Plan Status:" not in c:
360
- lines = c.splitlines()
361
- if len(lines) > 8:
362
- m["content"] = f"[Output: {len(lines)} lines truncated]\n" + "\n".join(lines[:3]) + "\n...\n" + "\n".join(lines[-2:])
363
- else:
364
- m["content"] = c[:200] + "... [truncated]"
359
+ # Condense bulky file write payloads in older assistant messages
360
+ last_asst_idx = -1
361
+ for i in range(len(recent_messages) - 1, -1, -1):
362
+ if recent_messages[i].get("role") == "assistant":
363
+ last_asst_idx = i
364
+ break
365
+
366
+ for i, m in enumerate(recent_messages):
367
+ if i != last_asst_idx and m.get("role") == "assistant":
368
+ if m.get("tool_calls"):
369
+ m["tool_calls"] = compact_tool_calls(m["tool_calls"])
370
+ if m.get("content"):
371
+ m["content"] = compact_text_tool_calls(m["content"])
372
+ elif m.get("role") == "tool":
373
+ c = str(m.get("content") or "")
374
+ if len(c) > 300 and "[Plan updated]" not in c and "Plan Status:" not in c:
375
+ lines = c.splitlines()
376
+ if len(lines) > 8:
377
+ top = "\n".join(lines[:3])
378
+ bot = "\n".join(lines[-2:])
379
+ m["content"] = f"[Output: {len(lines)} lines condensed]\n{top}\n...\n{bot}"
380
+ else:
381
+ m["content"] = c[:180] + "... [condensed]"
365
382
 
366
383
  context_blocks.extend(recent_messages)
367
384
 
368
385
  # ── Budget Verification ──
369
- effective_limit = max_tokens or self.max_tokens
370
- if estimate_tokens(context_blocks) > effective_limit:
371
- context_blocks = compact_messages(context_blocks)
386
+ reserved_headroom = 2048 if effective_limit <= 8192 else 4096
387
+ prompt_budget = max(1024, effective_limit - reserved_headroom)
388
+
389
+ if estimate_tokens(context_blocks) > prompt_budget:
390
+ context_blocks = compact_messages(context_blocks, keep_recent=4 if effective_limit <= 8192 else 8)
372
391
 
373
392
  if is_default_params:
374
393
  self._cached_context = [dict(m) for m in context_blocks]
@@ -403,6 +422,16 @@ class ContextManager:
403
422
  """Returns parallel metadata list for all messages."""
404
423
  return list(self._metadata)
405
424
 
425
+ def compact_for_headroom(self, target_headroom: int = 1536, n_ctx: int = 8192, tools_overhead: int = 800) -> int:
426
+ """
427
+ Actively compact context to ensure at least target_headroom tokens are available for generation.
428
+ Returns the new estimated token count.
429
+ """
430
+ self.invalidate_cache(reset_summary=True)
431
+ self._cached_summary = extract_conversation_summary(self._messages, self._metadata)
432
+ self._messages = compact_messages(self._messages, keep_recent=4)
433
+ return self.estimate_tokens()
434
+
406
435
  def estimate_tokens(self) -> int:
407
436
  """Estimate tokens of the active built context."""
408
437
  return estimate_tokens(self.build_context())
@@ -565,6 +565,11 @@ def gather_relevant_context(
565
565
  return None
566
566
 
567
567
  task_lower = task.lower().strip()
568
+
569
+ # Skip pre-reading repo files for standalone algorithms or scripts (e.g. palindrome, calculator)
570
+ standalone_kws = ["palindrome", "fibonacci", "quicksort", "bubblesort", "factorial", "hello world", "calculator", "reverse string", "reverse_string"]
571
+ if any(kw in task_lower for kw in standalone_kws):
572
+ return None
568
573
  sections: List[str] = []
569
574
  chars_used = 0
570
575