alpiecode 10.5.0__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 (49) hide show
  1. {alpiecode-10.5.0 → alpiecode-10.5.1}/PKG-INFO +1 -1
  2. {alpiecode-10.5.0 → alpiecode-10.5.1}/pyproject.toml +1 -1
  3. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode.egg-info/SOURCES.txt +3 -1
  5. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/__init__.py +1 -1
  6. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/backends/local_backend.py +31 -19
  7. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/backends/openai_backend.py +4 -0
  8. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/cli.py +26 -14
  9. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/compaction.py +122 -10
  10. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/config.py +2 -2
  11. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/context.py +43 -14
  12. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/dream_rsi.py +3 -2
  13. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/executor.py +10 -6
  14. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/local_model.py +38 -10
  15. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/orchestrator.py +85 -22
  16. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/prompt.py +5 -1
  17. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/system1.py +80 -16
  18. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/tools.py +27 -0
  19. alpiecode-10.5.1/test/test_context_and_trajectory.py +337 -0
  20. alpiecode-10.5.1/test/test_dream_rsi.py +478 -0
  21. {alpiecode-10.5.0 → alpiecode-10.5.1}/README.md +0 -0
  22. {alpiecode-10.5.0 → alpiecode-10.5.1}/setup.cfg +0 -0
  23. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode/__init__.py +0 -0
  24. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  25. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
  26. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode.egg-info/requires.txt +0 -0
  27. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/alpiecode.egg-info/top_level.txt +0 -0
  28. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/agent.py +0 -0
  29. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/backends/__init__.py +0 -0
  30. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/backends/base.py +0 -0
  31. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/cache.py +0 -0
  32. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/client.py +0 -0
  33. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/discovery.py +0 -0
  34. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/doctor.py +0 -0
  35. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/extension/alpiecode.vsix +0 -0
  36. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/git_ops.py +0 -0
  37. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/github.py +0 -0
  38. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/guardian.py +0 -0
  39. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/guardrails.py +0 -0
  40. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/ipython_ext.py +0 -0
  41. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/media.py +0 -0
  42. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/memory.py +0 -0
  43. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/metrics.py +0 -0
  44. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/progress.py +0 -0
  45. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/rephraser.py +0 -0
  46. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/server.py +0 -0
  47. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/session.py +0 -0
  48. {alpiecode-10.5.0 → alpiecode-10.5.1}/src/codeagent/updater.py +0 -0
  49. {alpiecode-10.5.0 → 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.5.0
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.5.0"
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.5.0
3
+ Version: 10.5.1
4
4
  Summary: AlpieCode
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -42,4 +42,6 @@ src/codeagent/backends/__init__.py
42
42
  src/codeagent/backends/base.py
43
43
  src/codeagent/backends/local_backend.py
44
44
  src/codeagent/backends/openai_backend.py
45
- src/codeagent/extension/alpiecode.vsix
45
+ src/codeagent/extension/alpiecode.vsix
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.5.0"
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."""
@@ -91,26 +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
- # Do not duplicate full file content in context history
103
- if name in ("write_file", "edit_file"):
104
- try:
105
- import json as _json
106
- a_dict = _json.loads(args) if isinstance(args, str) else args
107
- p = a_dict.get("path") or a_dict.get("filename") or ""
108
- args = f'{"path": "{p}"}'
109
- except Exception:
110
- args = "{...}"
111
- tc_descriptions.append(f"[Called tool: {name}({args})]")
112
- if tc_descriptions and "<tool_call>" not in content:
113
- 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()
114
126
  # Do NOT include tool_calls key — GGUF template will crash on it
115
127
 
116
128
  clean["content"] = content
@@ -54,9 +54,11 @@ 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
60
62
  dangling_bs = len(repaired) - len(repaired.rstrip('\\'))
61
63
  if dangling_bs % 2 != 0:
62
64
  repaired = repaired[:-1]
@@ -75,6 +77,8 @@ def _repair_and_parse_json_arguments(raw_args: str) -> dict:
75
77
  try:
76
78
  parsed = json.loads(repaired)
77
79
  if isinstance(parsed, dict):
80
+ if was_truncated:
81
+ parsed["_was_truncated"] = True
78
82
  return parsed
79
83
  except Exception:
80
84
  pass
@@ -31,16 +31,28 @@ def _show_banner():
31
31
  def _normalize_args():
32
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,14 +137,14 @@ 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
 
134
146
  # ?? dream ??
135
- dream_p = sub.add_parser('dream', help='Dream-RSI: offline recursive self-improvement simulation')
147
+ dream_p = sub.add_parser('dream', help='Dream-RSI: offline recursive self-improvement simulation', parents=[common])
136
148
  dream_p.add_argument('--optimize', action='store_true', help='Run offline dreaming simulation to synthesize optimal search policy')
137
149
  dream_p.add_argument('--stats', action='store_true', help='Show trajectory replay metrics and active policy status')
138
150
  dream_p.add_argument('--clear', action='store_true', help='Clear historical trajectory replay pool')
@@ -227,9 +239,9 @@ def main():
227
239
 
228
240
  if getattr(args, "offline", False):
229
241
  cfg.base_url = "offline"
230
- # 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
231
243
  import os
232
- cfg.n_ctx = int(os.environ.get("ALPIECODE_CTX_SIZE", 4096))
244
+ cfg.n_ctx = int(os.environ.get("ALPIECODE_CTX_SIZE", 8192))
233
245
  os.environ["ALPIECODE_BASE_URL"] = "offline"
234
246
 
235
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())
@@ -328,8 +328,9 @@ class DreamOptimizer:
328
328
  best_stats = {}
329
329
 
330
330
  # Exploration Policy Search Grid (Dream Simulation Space)
331
- syntax_thresholds = [1, 2]
332
- general_thresholds = [1, 2, 3]
331
+ # Minimum threshold = 2 to prevent false positive loop detection on first call
332
+ syntax_thresholds = [2, 3]
333
+ general_thresholds = [2, 3, 4]
333
334
  stall_thresholds = [2, 3, 4]
334
335
  single_file_options = [True, False]
335
336
  nudge_options = [True, False]
@@ -51,11 +51,13 @@ def parse_text_tool_calls(text: str) -> List[dict]:
51
51
  if isinstance(data, dict) and "name" in data:
52
52
  args = data.get("arguments")
53
53
  if not isinstance(args, dict) or not args:
54
- fallback_args = {k: v for k, v in data.items() if k not in ("name", "id", "type", "_raw_arguments", "_parse_error")}
54
+ fallback_args = {k: v for k, v in data.items() if k not in ("name", "id", "type", "_raw_arguments", "_parse_error", "_was_truncated")}
55
55
  if fallback_args:
56
56
  args = fallback_args
57
57
  elif not isinstance(args, dict):
58
58
  args = {}
59
+ if data.get("_was_truncated"):
60
+ args["_was_truncated"] = True
59
61
  tool_calls.append({
60
62
  "name": data["name"],
61
63
  "arguments": args
@@ -148,11 +150,13 @@ def _parse_error_hint(stderr: str, stdout: str) -> str:
148
150
  )
149
151
  elif "command not found" in combined:
150
152
  cmd_match = combined.split("command not found")[0].strip().split()
151
- cmd = cmd_match[-1] if cmd_match else "unknown"
152
- hints.append(
153
- f"Hint: Command '{cmd}' not found. It may not be installed or not in PATH. "
154
- "Check available commands or install the required package."
155
- )
153
+ cmd = cmd_match[-1].rstrip(":") if cmd_match else "unknown"
154
+ hint = f"Hint: Command '{cmd}' not found. It may not be installed or not in PATH."
155
+ if cmd in ("node", "npm", "npx"):
156
+ hint += " For client-side web projects (HTML/CSS/JS), files do not require node to run. Output DONE: <summary> when complete."
157
+ else:
158
+ hint += " Check available commands or install the required package."
159
+ hints.append(hint)
156
160
 
157
161
  return "\n".join(hints)
158
162
 
@@ -463,17 +463,34 @@ class LocalModel:
463
463
  msgs = messages
464
464
 
465
465
  # Context headroom safety guard: dynamically clamp max_tokens so prompt + max_tokens <= n_ctx
466
- n_ctx_limit = getattr(self, "n_ctx", 4096)
466
+ n_ctx_limit = getattr(self, "n_ctx", 8192)
467
467
  try:
468
- total_chars = sum(len(str(m.get("content", ""))) for m in msgs)
468
+ total_chars = 0
469
+ for m in msgs:
470
+ total_chars += len(str(m.get("content") or ""))
471
+ for tc in m.get("tool_calls") or []:
472
+ if isinstance(tc, dict):
473
+ fn = tc.get("function", {})
474
+ total_chars += len(str(fn.get("arguments", ""))) + len(str(fn.get("name", "")))
469
475
  if tools:
470
476
  import json
471
477
  total_chars += len(json.dumps(tools))
472
- # Conservative token estimate: 1 token ≈ 3.0 chars + tool/template overhead
473
- est_prompt_tokens = int(total_chars / 3.0) + 160
474
- available_tokens = n_ctx_limit - est_prompt_tokens - 48
475
- if max_tokens > available_tokens:
476
- max_tokens = max(64, available_tokens)
478
+ # Accurate token estimate: 1 token ≈ 3.2 chars + template overhead
479
+ est_prompt_tokens = int(total_chars / 3.2) + 120
480
+ available_tokens = n_ctx_limit - est_prompt_tokens - 32
481
+ if available_tokens < 1024 and len(msgs) > 2:
482
+ from .compaction import compact_messages
483
+ msgs = compact_messages(msgs, keep_recent=2)
484
+ total_chars = sum(len(str(m.get("content") or "")) for m in msgs)
485
+ if tools:
486
+ import json
487
+ total_chars += len(json.dumps(tools))
488
+ est_prompt_tokens = int(total_chars / 3.2) + 120
489
+ available_tokens = n_ctx_limit - est_prompt_tokens - 32
490
+
491
+ safe_cap = max(256, available_tokens)
492
+ if max_tokens > safe_cap:
493
+ max_tokens = safe_cap
477
494
  except Exception:
478
495
  pass
479
496
 
@@ -506,7 +523,13 @@ class LocalModel:
506
523
  requested = int(m.group(1))
507
524
  win = int(m.group(2))
508
525
  excess = requested - win
509
- if cur_max - excess - 32 >= 64:
526
+ if excess > 0 and len(params.get("messages", [])) > 2:
527
+ from .compaction import compact_messages
528
+ params["messages"] = compact_messages(params["messages"], keep_recent=2)
529
+ cur_max = min(2048, max(256, win - int(requested - excess) // 2))
530
+ params["max_tokens"] = cur_max
531
+ continue
532
+ elif cur_max - excess - 32 >= 64:
510
533
  cur_max = max(64, cur_max - excess - 32)
511
534
  continue
512
535
  raise
@@ -526,8 +549,13 @@ class LocalModel:
526
549
  requested = int(m.group(1))
527
550
  win = int(m.group(2))
528
551
  excess = requested - win
529
- new_max = max(64, params["max_tokens"] - excess - 32)
530
- params["max_tokens"] = new_max
552
+ if excess > 0 and len(params.get("messages", [])) > 2:
553
+ from .compaction import compact_messages
554
+ params["messages"] = compact_messages(params["messages"], keep_recent=2)
555
+ params["max_tokens"] = min(2048, max(512, win - int(requested - excess) // 2))
556
+ else:
557
+ new_max = max(128, params["max_tokens"] - excess - 32)
558
+ params["max_tokens"] = new_max
531
559
  result_dict = llm.create_chat_completion(**params)
532
560
  else:
533
561
  raise